tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.__lock = Lock()  # initialize multiprocessing mutex lock
 129
 130        self.aliases = TKS_TICKER_ALIASES
 131        """Some aliases instead official tickers.
 132
 133        See also: `TKSEnums.TKS_TICKER_ALIASES`
 134        """
 135
 136        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 137
 138        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 139
 140        self._ticker = ""
 141        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 142
 143        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 144        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 145
 146        See also: `SearchByTicker()`, `SearchInstruments()`.
 147        """
 148
 149        self._figi = ""
 150        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 151
 152        See also: `SearchByFIGI()`, `SearchInstruments()`.
 153        """
 154
 155        self.depth = 1
 156        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 157
 158        See also: `GetCurrentPrices()`.
 159        """
 160
 161        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 162        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 163
 164        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 165        """
 166
 167        uLogger.debug("Broker API server: {}".format(self.server))
 168
 169        self.timeout = 15
 170        """Server operations timeout in seconds. Default: `15`.
 171
 172        See also: `SendAPIRequest()`.
 173        """
 174
 175        self.headers = {
 176            "Content-Type": "application/json",
 177            "accept": "application/json",
 178            "Authorization": "Bearer {}".format(self.token),
 179            "x-app-name": "Tim55667757.TKSBrokerAPI",
 180        }
 181        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 182
 183        See also: `SendAPIRequest()`.
 184        """
 185
 186        self.body = None
 187        """Request body which send to broker server. Default: `None`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.moreDebug = False
 193        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 194
 195        self.useHTMLReports = False
 196        """
 197        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 198        
 199        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 200        """
 201
 202        self.historyFile = None
 203        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 204
 205        See also: `History()`.
 206        """
 207
 208        self.htmlHistoryFile = "index.html"
 209        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 210
 211        See also: `ShowHistoryChart()`.
 212        """
 213
 214        self.instrumentsFile = "instruments.md"
 215        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 216
 217        See also: `ShowInstrumentsInfo()`.
 218        """
 219
 220        self.searchResultsFile = "search-results.md"
 221        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 222
 223        See also: `SearchInstruments()`.
 224        """
 225
 226        self.pricesFile = "prices.md"
 227        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 228
 229        See also: `GetListOfPrices()`.
 230        """
 231
 232        self.infoFile = "info.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 236        """
 237
 238        self.bondsXLSXFile = "ext-bonds.xlsx"
 239        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 240        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 241
 242        See also: `ExtendBondsData()`.
 243        """
 244
 245        self.calendarFile = "calendar.md"
 246        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 247        
 248        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 249
 250        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 251        """
 252
 253        self.overviewFile = "overview.md"
 254        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 255
 256        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 257        """
 258
 259        self.overviewDigestFile = "overview-digest.md"
 260        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 261
 262        See also: `Overview()` with parameter `details="digest"`.
 263        """
 264
 265        self.overviewPositionsFile = "overview-positions.md"
 266        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 267
 268        See also: `Overview()` with parameter `details="positions"`.
 269        """
 270
 271        self.overviewOrdersFile = "overview-orders.md"
 272        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 273
 274        See also: `Overview()` with parameter `details="orders"`.
 275        """
 276
 277        self.overviewAnalyticsFile = "overview-analytics.md"
 278        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 279
 280        See also: `Overview()` with parameter `details="analytics"`.
 281        """
 282
 283        self.overviewBondsCalendarFile = "overview-calendar.md"
 284        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 285
 286        See also: `Overview()` with parameter `details="calendar"`.
 287        """
 288
 289        self.reportFile = "deals.md"
 290        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 291
 292        See also: `Deals()`.
 293        """
 294
 295        self.withdrawalLimitsFile = "limits.md"
 296        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 297
 298        See also: `OverviewLimits()` and `RequestLimits()`.
 299        """
 300
 301        self.userInfoFile = "user-info.md"
 302        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 303
 304        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 305        """
 306
 307        self.userAccountsFile = "accounts.md"
 308        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 309
 310        See also: `OverviewAccounts()`, `RequestAccounts()`.
 311        """
 312
 313        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 314        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 315
 316        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 317
 318        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 319        """
 320
 321        self.iList = None  # init iList for raw instruments data
 322        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 323        
 324        See also: `Listing()`, `DumpInstruments()`.
 325        """
 326
 327        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 328        if useCache:
 329            if os.path.exists(self.iListDumpFile):
 330                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 331                curTime = datetime.now(tzutc())
 332
 333                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 334                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 335
 336                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 337
 338                else:
 339                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 340
 341                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 342                        os.path.abspath(self.iListDumpFile),
 343                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 344                    ))
 345
 346            else:
 347                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 348                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 349
 350        else:
 351            self.iList = self.Listing()  # request new raw instruments data from broker server
 352            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 353
 354        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 355        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 356
 357        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 358        """
 359
 360    @property
 361    def ticker(self) -> str:
 362        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        return self._ticker
 370
 371    @ticker.setter
 372    def ticker(self, value):
 373        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 374
 375        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 376        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 377
 378        See also: `SearchByTicker()`, `SearchInstruments()`.
 379        """
 380        self._ticker = str(value).upper()  # Tickers may be upper case only
 381
 382    @property
 383    def figi(self) -> str:
 384        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 385
 386        See also: `SearchByFIGI()`, `SearchInstruments()`.
 387        """
 388        return self._figi
 389
 390    @figi.setter
 391    def figi(self, value):
 392        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 393
 394        See also: `SearchByFIGI()`, `SearchInstruments()`.
 395        """
 396        self._figi = str(value).upper()  # FIGI may be upper case only
 397
 398    def _ParseJSON(self, rawData="{}") -> dict:
 399        """
 400        Parse JSON from response string.
 401
 402        :param rawData: this is a string with JSON-formatted text.
 403        :return: JSON (dictionary), parsed from server response string.
 404        """
 405        responseJSON = json.loads(rawData) if rawData else {}
 406
 407        if self.moreDebug:
 408            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 409
 410        return responseJSON
 411
 412    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 413        """
 414        Send GET or POST request to broker server and receive JSON object.
 415
 416        self.header: must be defining with dictionary of headers.
 417        self.body: if define then used as request body. None by default.
 418        self.timeout: global request timeout, 15 seconds by default.
 419        :param url: url with REST request.
 420        :param reqType: send "GET" or "POST" request. "GET" by default.
 421        :param retry: how many times retry after first request if an 5xx server errors occurred.
 422        :param pause: sleep time in seconds between retries.
 423        :return: response JSON (dictionary) from broker.
 424        """
 425        if reqType.upper() not in ("GET", "POST"):
 426            uLogger.error("You can define request type: `GET` or `POST`!")
 427            raise Exception("Incorrect value")
 428
 429        if self.moreDebug:
 430            uLogger.debug("Request parameters:")
 431            uLogger.debug("    - REST API URL: {}".format(url))
 432            uLogger.debug("    - request type: {}".format(reqType))
 433            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 434            uLogger.debug("    - body:\n{}".format(self.body))
 435
 436        # fast hack to avoid all operations with some tickers/FIGI
 437        responseJSON = {}
 438        oK = True
 439        for item in self.exclude:
 440            if item in url:
 441                if self.moreDebug:
 442                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 443
 444                oK = False
 445                break
 446
 447        if oK:
 448            with self.__lock:  # acquire the mutex lock
 449                counter = 0
 450                response = None
 451                errMsg = ""
 452
 453                while not response and counter <= retry:
 454                    if reqType == "GET":
 455                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 456
 457                    if reqType == "POST":
 458                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 459
 460                    if self.moreDebug:
 461                        uLogger.debug("Response:")
 462                        uLogger.debug("    - status code: {}".format(response.status_code))
 463                        uLogger.debug("    - reason: {}".format(response.reason))
 464                        uLogger.debug("    - body length: {}".format(len(response.text)))
 465                        uLogger.debug("    - headers:\n{}".format(response.headers))
 466
 467                    # Server returns some headers:
 468                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 469                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 470                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 471                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 472                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 473                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 474                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 475                        sleep(rateLimitWait)
 476
 477                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 478                    if 400 <= response.status_code < 500:
 479                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 480                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 481
 482                        if "code" in response.text and "message" in response.text:
 483                            msgDict = self._ParseJSON(rawData=response.text)
 484                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 485
 486                        counter = retry + 1  # do not retry for 4xx errors
 487
 488                    if 500 <= response.status_code < 600:
 489                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 490                        uLogger.debug("    - not oK, {}".format(errMsg))
 491
 492                        if "code" in response.text and "message" in response.text:
 493                            errMsgDict = self._ParseJSON(rawData=response.text)
 494                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 495
 496                        counter += 1
 497
 498                        if counter <= retry:
 499                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 500                            sleep(pause)
 501
 502                responseJSON = self._ParseJSON(rawData=response.text)
 503
 504                if errMsg:
 505                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 506                    uLogger.error("    - not oK, {}".format(errMsg))
 507
 508        return responseJSON
 509
 510    def _IUpdater(self, iType: str) -> tuple:
 511        """
 512        Request instrument by type from server. See available API methods for instruments:
 513        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 514        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 515        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 516        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 517        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 518
 519        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 520        :return: tuple with iType name and list of available instruments of current type for defined user token.
 521        """
 522        result = []
 523
 524        if iType in TKS_INSTRUMENTS:
 525            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 526
 527            # all instruments have the same body in API v2 requests:
 528            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 529            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 530            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 531
 532        return iType, result
 533
 534    def _IWrapper(self, kwargs):
 535        """
 536        Wrapper runs instrument's update method `_IUpdater()`.
 537        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 538        """
 539        return self._IUpdater(**kwargs)
 540
 541    def Listing(self) -> dict:
 542        """
 543        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 544
 545        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 546        """
 547        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 548        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 549
 550        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 551        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 552        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 553
 554        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 555        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 556        poolUpdater.close()  # close the thread pool
 557        poolUpdater.join()  # wait a moment until all data returns from threads
 558
 559        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 560        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 561        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 562
 563        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 564        for iType in iList.keys():
 565            for ticker in iList[iType]:
 566                iList[iType][ticker]["type"] = iType
 567
 568                if "minPriceIncrement" in iList[iType][ticker].keys():
 569                    iList[iType][ticker]["step"] = NanoToFloat(
 570                        iList[iType][ticker]["minPriceIncrement"]["units"],
 571                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 572                    )
 573
 574                else:
 575                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 576
 577        return iList
 578
 579    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 580        """
 581        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 582
 583        See also: `DumpInstruments()`, `Listing()`.
 584
 585        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 586                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 587        """
 588        if self.iListDumpFile is None or not self.iListDumpFile:
 589            uLogger.error("Output name of dump file must be defined!")
 590            raise Exception("Filename required")
 591
 592        if not self.iList or forceUpdate:
 593            self.iList = self.Listing()
 594
 595        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 596
 597        # Save as XLSX with separated sheets for every type of instruments:
 598        with pd.ExcelWriter(
 599                path=xlsxDumpFile,
 600                date_format=TKS_DATE_FORMAT,
 601                datetime_format=TKS_DATE_TIME_FORMAT,
 602                mode="w",
 603        ) as writer:
 604            for iType in TKS_INSTRUMENTS:
 605                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 606                df = df[sorted(df)]  # sorted by column names
 607                df = df.applymap(
 608                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 609                    na_action="ignore",
 610                )  # converting numbers from nano-type to float in every cell
 611                df.to_excel(
 612                    writer,
 613                    sheet_name=iType,
 614                    encoding="UTF-8",
 615                    freeze_panes=(1, 1),
 616                )  # saving as XLSX-file with freeze first row and column as headers
 617
 618        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 619
 620    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 621        """
 622        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 623        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 624
 625        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 626
 627        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 628                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 629        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 630        """
 631        if self.iListDumpFile is None or not self.iListDumpFile:
 632            uLogger.error("Output name of dump file must be defined!")
 633            raise Exception("Filename required")
 634
 635        if not self.iList or forceUpdate:
 636            self.iList = self.Listing()
 637
 638        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 639        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 640            fH.write(jsonDump)
 641
 642        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 643
 644        return jsonDump
 645
 646    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 647        """
 648        Show information about one instrument defined by json data and prints it in Markdown format.
 649
 650        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 651
 652        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 653        :param show: if `True` then also printing information about instrument and its current price.
 654        :return: multilines text in Markdown format with information about one instrument.
 655        """
 656        splitLine = "|                                                             |                                                        |\n"
 657        infoText = ""
 658
 659        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 660            info = [
 661                "# Main information\n\n",
 662                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 663                "| Parameters                                                  | Values                                                 |\n",
 664                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 665                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 666                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 667            ]
 668
 669            if "sector" in iJSON.keys() and iJSON["sector"]:
 670                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 671
 672            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 673                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 674
 675            info.extend([
 676                splitLine,
 677                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 678                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 679            ])
 680
 681            if "isin" in iJSON.keys() and iJSON["isin"]:
 682                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 683
 684            if "classCode" in iJSON.keys():
 685                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 686
 687            info.extend([
 688                splitLine,
 689                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 690                splitLine,
 691                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 692                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 693                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 694            ])
 695
 696            if iJSON["figi"]:
 697                self._figi = iJSON["figi"]
 698                iJSON = iJSON | self.RequestTradingStatus()
 699
 700                info.extend([
 701                    splitLine,
 702                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 703                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 704                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 705                ])
 706
 707            info.append(splitLine)
 708
 709            if "type" in iJSON.keys() and iJSON["type"]:
 710                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 711
 712                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 713                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 714
 715            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 716                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 717
 718            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 719                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 720
 721            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 722                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 723
 724            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 725                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 726
 727            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 728                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 729
 730            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 731                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 732
 733            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 734                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 735
 736            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 737                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 738
 739            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 740                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 741
 742            if "currency" in iJSON.keys():
 743                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 744
 745            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 746                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 747
 748            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 749                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 750
 751            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 752                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 753
 754            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 755                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 756
 757            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 758                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 761                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 764                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 767                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 768
 769            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 770                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 771
 772            iExt = None
 773            if iJSON["type"] == "Bonds":
 774                info.extend([
 775                    splitLine,
 776                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 777                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 778                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 779                        iJSON["nominal"]["currency"],
 780                    )),
 781                ])
 782
 783                if "floatingCouponFlag" in iJSON.keys():
 784                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 785
 786                if "amortizationFlag" in iJSON.keys():
 787                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 788
 789                info.append(splitLine)
 790
 791                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 792                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 793
 794                if iJSON["figi"]:
 795                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 796
 797                    info.extend([
 798                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 799                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 800                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 801                    ])
 802
 803                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 804                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 805                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 806                        iJSON["aciValue"]["currency"]
 807                    )))
 808
 809            if "currentPrice" in iJSON.keys():
 810                info.append(splitLine)
 811
 812                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 813                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 814
 815                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 816                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 817                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 818                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 819                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 820
 821                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 822                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 823
 824                info.extend([
 825                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 826                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 827                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 828                    )),
 829                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 830                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 831                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 832                    )),
 833                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 834                        "{:.2f}%{}".format(
 835                            iJSON["currentPrice"]["changes"],
 836                            " ({}{:.2f} {})".format(
 837                                "+" if bondChangesDelta > 0 else "",
 838                                bondChangesDelta,
 839                                aciCurrency
 840                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 841                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 842                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 843                                currency
 844                            ),
 845                        )
 846                    ),
 847                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 848                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 849                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 850                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 851                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 852                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 853                    )),
 854                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 855                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 856                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 857                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 858                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 859                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 860                    )),
 861                ])
 862
 863            if "lot" in iJSON.keys():
 864                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 865
 866            if "step" in iJSON.keys() and iJSON["step"] != 0:
 867                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 868
 869            # Add bond payment calendar:
 870            if iJSON["type"] == "Bonds":
 871                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 872                info.extend(["\n#", strCalendar])
 873
 874            infoText += "".join(info)
 875
 876            if show:
 877                uLogger.info("{}".format(infoText))
 878
 879            else:
 880                uLogger.debug("{}".format(infoText))
 881
 882            if self.infoFile is not None:
 883                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 884                    fH.write(infoText)
 885
 886                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 887
 888                if self.useHTMLReports:
 889                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 890                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 891                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 892
 893                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 894
 895        return infoText
 896
 897    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 898        """
 899        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 900
 901        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 902        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 903        :return: JSON formatted data with information about instrument.
 904        """
 905        tickerJSON = {}
 906        if self.moreDebug:
 907            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 908
 909        if not self._ticker:
 910            uLogger.warning("self._ticker variable is not be empty!")
 911
 912        else:
 913            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 914                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 915                raise Exception("Instrument not allowed")
 916
 917            if not self.iList:
 918                self.iList = self.Listing()
 919
 920            if self._ticker in self.iList["Shares"].keys():
 921                tickerJSON = self.iList["Shares"][self._ticker]
 922                if self.moreDebug:
 923                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 924
 925            elif self._ticker in self.iList["Currencies"].keys():
 926                tickerJSON = self.iList["Currencies"][self._ticker]
 927                if self.moreDebug:
 928                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 929
 930            elif self._ticker in self.iList["Bonds"].keys():
 931                tickerJSON = self.iList["Bonds"][self._ticker]
 932                if self.moreDebug:
 933                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 934
 935            elif self._ticker in self.iList["Etfs"].keys():
 936                tickerJSON = self.iList["Etfs"][self._ticker]
 937                if self.moreDebug:
 938                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 939
 940            elif self._ticker in self.iList["Futures"].keys():
 941                tickerJSON = self.iList["Futures"][self._ticker]
 942                if self.moreDebug:
 943                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 944
 945        if tickerJSON:
 946            self._figi = tickerJSON["figi"]
 947
 948            if requestPrice:
 949                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 950
 951                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 952                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 953
 954                else:
 955                    tickerJSON["currentPrice"]["changes"] = 0
 956
 957            if show:
 958                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 959
 960        else:
 961            if show:
 962                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 963
 964        return tickerJSON
 965
 966    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 967        """
 968        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 969
 970        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 971        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 972        :return: JSON formatted data with information about instrument.
 973        """
 974        figiJSON = {}
 975        if self.moreDebug:
 976            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 977
 978        if not self._figi:
 979            uLogger.warning("self._figi variable is not be empty!")
 980
 981        else:
 982            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 983                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 984                raise Exception("Instrument not allowed")
 985
 986            if not self.iList:
 987                self.iList = self.Listing()
 988
 989            for item in self.iList["Shares"].keys():
 990                if self._figi == self.iList["Shares"][item]["figi"]:
 991                    figiJSON = self.iList["Shares"][item]
 992
 993                    if self.moreDebug:
 994                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 995
 996                    break
 997
 998            if not figiJSON:
 999                for item in self.iList["Currencies"].keys():
1000                    if self._figi == self.iList["Currencies"][item]["figi"]:
1001                        figiJSON = self.iList["Currencies"][item]
1002
1003                        if self.moreDebug:
1004                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1005
1006                        break
1007
1008            if not figiJSON:
1009                for item in self.iList["Bonds"].keys():
1010                    if self._figi == self.iList["Bonds"][item]["figi"]:
1011                        figiJSON = self.iList["Bonds"][item]
1012
1013                        if self.moreDebug:
1014                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1015
1016                        break
1017
1018            if not figiJSON:
1019                for item in self.iList["Etfs"].keys():
1020                    if self._figi == self.iList["Etfs"][item]["figi"]:
1021                        figiJSON = self.iList["Etfs"][item]
1022
1023                        if self.moreDebug:
1024                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1025
1026                        break
1027
1028            if not figiJSON:
1029                for item in self.iList["Futures"].keys():
1030                    if self._figi == self.iList["Futures"][item]["figi"]:
1031                        figiJSON = self.iList["Futures"][item]
1032
1033                        if self.moreDebug:
1034                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1035
1036                        break
1037
1038        if figiJSON:
1039            self._figi = figiJSON["figi"]
1040            self._ticker = figiJSON["ticker"]
1041
1042            if requestPrice:
1043                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1044
1045                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1046                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1047
1048                else:
1049                    figiJSON["currentPrice"]["changes"] = 0
1050
1051            if show:
1052                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1053
1054        else:
1055            if show:
1056                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1057
1058        return figiJSON
1059
1060    def GetCurrentPrices(self, show: bool = True) -> dict:
1061        """
1062        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1063        `{"buy": [{"price": 1243.8, "quantity": 193},
1064                  {"price": 1244.0, "quantity": 168},
1065                  {"price": 1244.8, "quantity": 5},
1066                  {"price": 1245.0, "quantity": 61},
1067                  {"price": 1245.4, "quantity": 60}],
1068          "sell": [{"price": 1243.6, "quantity": 8},
1069                   {"price": 1242.6, "quantity": 10},
1070                   {"price": 1242.4, "quantity": 18},
1071                   {"price": 1242.2, "quantity": 50},
1072                   {"price": 1242.0, "quantity": 113}],
1073          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1074        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1075        - sell: list of dicts with Buyers prices,
1076            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1077            - quantity: volume value by current price in lots,
1078        - limitUp: current trade session limit price, maximum,
1079        - limitDown: current trade session limit price, minimum,
1080        - lastPrice: last deal price of the instrument,
1081        - closePrice: previous trade session close price of the instrument.
1082
1083        See also: `SearchByTicker()` and `SearchByFIGI()`.
1084        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1085        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1086
1087        :param show: if `True` then print DOM to log and console.
1088        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1089                 If an error occurred then returns an empty record:
1090                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1091        """
1092        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1093
1094        if self.depth < 1:
1095            uLogger.error("Depth of Market (DOM) must be >=1!")
1096            raise Exception("Incorrect value")
1097
1098        if not (self._ticker or self._figi):
1099            uLogger.error("self._ticker or self._figi variables must be defined!")
1100            raise Exception("Ticker or FIGI required")
1101
1102        if self._ticker and not self._figi:
1103            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1104            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1105
1106        if not self._ticker and self._figi:
1107            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1108            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1109
1110        if not self._figi:
1111            uLogger.error("FIGI is not defined!")
1112            raise Exception("Ticker or FIGI required")
1113
1114        else:
1115            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1116
1117            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1118            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1119            self.body = str({"figi": self._figi, "depth": self.depth})
1120            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1121
1122            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1123                # list of dicts with sellers orders:
1124                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1125
1126                # list of dicts with buyers orders:
1127                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1128
1129                # max price of instrument at this time:
1130                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1131
1132                # min price of instrument at this time:
1133                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1134
1135                # last price of deal with instrument:
1136                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1137
1138                # last close price of instrument:
1139                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1140
1141            else:
1142                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1143                uLogger.debug("Server response: {}".format(pricesResponse))
1144
1145            if show:
1146                if prices["buy"] or prices["sell"]:
1147                    info = [
1148                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1149                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1150                            self._ticker,
1151                            self._figi,
1152                            self.depth,
1153                        ),
1154                        "-" * 60, "\n",
1155                        "             Orders of Buyers | Orders of Sellers\n",
1156                        "-" * 60, "\n",
1157                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1158                        "-" * 60, "\n",
1159                    ]
1160
1161                    if not prices["buy"]:
1162                        info.append("                              | No orders!\n")
1163                        sumBuy = 0
1164
1165                    else:
1166                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1167                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1168                        for item in maxMinSorted:
1169                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1170
1171                    if not prices["sell"]:
1172                        info.append("No orders!                    |\n")
1173                        sumSell = 0
1174
1175                    else:
1176                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1177                        for item in prices["sell"]:
1178                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1179
1180                    info.extend([
1181                        "-" * 60, "\n",
1182                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1183                        "-" * 60, "\n",
1184                    ])
1185
1186                    infoText = "".join(info)
1187
1188                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1189
1190                else:
1191                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1192
1193        return prices
1194
1195    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1196        """
1197        This method get and show information about all available broker instruments for current user account.
1198        If `instrumentsFile` string is not empty then also save information to this file.
1199
1200        :param show: if `True` then print results to console, if `False` — print only to file.
1201        :return: multi-lines string with all available broker instruments
1202        """
1203        if not self.iList:
1204            self.iList = self.Listing()
1205
1206        info = [
1207            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1208            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1209        ]
1210
1211        # add instruments count by type:
1212        for iType in self.iList.keys():
1213            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1214
1215        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1216        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1217
1218        # generating info tables with all instruments by type:
1219        for iType in self.iList.keys():
1220            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1221
1222            for instrument in self.iList[iType].keys():
1223                iName = self.iList[iType][instrument]["name"]  # instrument's name
1224                if len(iName) > 57:
1225                    iName = "{}...".format(iName[:54])  # right trim for a long string
1226
1227                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1228                    self.iList[iType][instrument]["ticker"],
1229                    iName,
1230                    self.iList[iType][instrument]["figi"],
1231                    self.iList[iType][instrument]["currency"],
1232                    self.iList[iType][instrument]["lot"],
1233                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1234                ))
1235
1236        infoText = "".join(info)
1237
1238        if show:
1239            uLogger.info(infoText)
1240
1241        if self.instrumentsFile:
1242            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1243                fH.write(infoText)
1244
1245            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1246
1247            if self.useHTMLReports:
1248                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1249                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1250                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1251
1252                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1253
1254        return infoText
1255
1256    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1257        """
1258        This method search and show information about instruments by part of its ticker, FIGI or name.
1259        If `searchResultsFile` string is not empty then also save information to this file.
1260
1261        :param pattern: string with part of ticker, FIGI or instrument's name.
1262        :param show: if `True` then print results to console, if `False` — return list of result only.
1263        :return: list of dictionaries with all found instruments.
1264        """
1265        if not self.iList:
1266            self.iList = self.Listing()
1267
1268        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1269        compiledPattern = re.compile(pattern, re.IGNORECASE)
1270
1271        for iType in self.iList:
1272            for instrument in self.iList[iType].values():
1273                searchResult = compiledPattern.search(" ".join(
1274                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1275                ))
1276
1277                if searchResult:
1278                    searchResults[iType][instrument["ticker"]] = instrument
1279
1280        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1281        info = [
1282            "# Search results\n\n",
1283            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1284            "* **Search pattern:** [{}]\n".format(pattern),
1285            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1286            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1287        ]
1288        infoShort = info[:]
1289
1290        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1291        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1292        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1293
1294        if resultsLen == 0:
1295            info.append("\nNo results\n")
1296            infoShort.append("\nNo results\n")
1297            uLogger.warning("No results. Try changing your search pattern.")
1298
1299        else:
1300            for iType in searchResults:
1301                iTypeValuesCount = len(searchResults[iType].values())
1302                if iTypeValuesCount > 0:
1303                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1304                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305
1306                    for instrument in searchResults[iType].values():
1307                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1308                            instrument["type"],
1309                            instrument["ticker"],
1310                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1311                            instrument["figi"],
1312                        ))
1313
1314                    if iTypeValuesCount <= 5:
1315                        infoShort.extend(info[-iTypeValuesCount:])
1316
1317                    else:
1318                        infoShort.extend(info[-5:])
1319                        infoShort.append(skippedLine)
1320
1321        infoText = "".join(info)
1322        infoTextShort = "".join(infoShort)
1323
1324        if show:
1325            uLogger.info(infoTextShort)
1326            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1327
1328        if self.searchResultsFile:
1329            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1330                fH.write(infoText)
1331
1332            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1333
1334            if self.useHTMLReports:
1335                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1336                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1337                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1338
1339                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1340
1341        return searchResults
1342
1343    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344        """
1345        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1346
1347        :param instruments: list of strings with tickers or FIGIs.
1348        :return: list with unique instrument FIGIs only.
1349        """
1350        requestedInstruments = []
1351        for iName in instruments:
1352            if iName not in self.aliases.keys():
1353                if iName not in requestedInstruments:
1354                    requestedInstruments.append(iName)
1355
1356            else:
1357                if iName not in requestedInstruments:
1358                    if self.aliases[iName] not in requestedInstruments:
1359                        requestedInstruments.append(self.aliases[iName])
1360
1361        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1362
1363        onlyUniqueFIGIs = []
1364        for iName in requestedInstruments:
1365            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1366                continue
1367
1368            self._ticker = iName
1369            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1370
1371            if not iData:
1372                self._ticker = ""
1373                self._figi = iName
1374
1375                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1376
1377                if not iData:
1378                    self._figi = ""
1379                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1380
1381            if iData and iData["figi"] not in onlyUniqueFIGIs:
1382                onlyUniqueFIGIs.append(iData["figi"])
1383
1384        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1385
1386        return onlyUniqueFIGIs
1387
1388    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1389        """
1390        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1391
1392        See limits: https://tinkoff.github.io/investAPI/limits/
1393
1394        If `pricesFile` string is not empty then also save information to this file.
1395
1396        :param instruments: list of strings with tickers or FIGIs.
1397        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1398        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1399                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1400        """
1401        if instruments is None or not instruments:
1402            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1403            raise Exception("Ticker or FIGI required")
1404
1405        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1406
1407        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1408
1409        iList = []  # trying to get info and current prices about all unique instruments:
1410        for self._figi in onlyUniqueFIGIs:
1411            iData = self.SearchByFIGI(requestPrice=True)
1412            iList.append(iData)
1413
1414        self.ShowListOfPrices(iList, show)
1415
1416        return iList
1417
1418    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1419        """
1420        Show table contains current prices of given instruments.
1421
1422        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1423                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1424        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1425        :return: multilines text in Markdown format as a table contains current prices.
1426        """
1427        infoText = ""
1428
1429        if show or self.pricesFile:
1430            info = [
1431                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1432                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1433                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1434            ]
1435
1436            for item in iList:
1437                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1438                    item["ticker"],
1439                    item["figi"],
1440                    item["type"],
1441                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1442                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1443                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1444                    "{} / {}".format(
1445                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1446                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1447                    ),
1448                    "{} / {}".format(
1449                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1450                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1451                    ),
1452                    item["currency"],
1453                ))
1454
1455            infoText = "".join(info)
1456
1457            if show:
1458                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1459
1460            if self.pricesFile:
1461                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1462                    fH.write(infoText)
1463
1464                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1465
1466                if self.useHTMLReports:
1467                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1468                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1469                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1470
1471                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1472
1473        return infoText
1474
1475    def RequestTradingStatus(self) -> dict:
1476        """
1477        Requesting trading status for the instrument defined by `figi` variable.
1478
1479        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1480
1481        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1482
1483        :return: dictionary with trading status attributes. Response example:
1484                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1485                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1486        """
1487        if self._figi is None or not self._figi:
1488            uLogger.error("Variable `figi` must be defined for using this method!")
1489            raise Exception("FIGI required")
1490
1491        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1492
1493        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1494        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1495        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1496
1497        if self.moreDebug:
1498            uLogger.debug("Records about current trading status successfully received")
1499
1500        return tradingStatus
1501
1502    def RequestPortfolio(self) -> dict:
1503        """
1504        Requesting actual user's portfolio for current `accountId`.
1505
1506        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1507
1508        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1509
1510        :return: dictionary with user's portfolio.
1511        """
1512        if self.accountId is None or not self.accountId:
1513            uLogger.error("Variable `accountId` must be defined for using this method!")
1514            raise Exception("Account ID required")
1515
1516        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1517
1518        self.body = str({"accountId": self.accountId})
1519        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1520        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1521
1522        if self.moreDebug:
1523            uLogger.debug("Records about user's portfolio successfully received")
1524
1525        return rawPortfolio
1526
1527    def RequestPositions(self) -> dict:
1528        """
1529        Requesting open positions by currencies and instruments for current `accountId`.
1530
1531        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1532
1533        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1534
1535        :return: dictionary with open positions by instruments.
1536        """
1537        if self.accountId is None or not self.accountId:
1538            uLogger.error("Variable `accountId` must be defined for using this method!")
1539            raise Exception("Account ID required")
1540
1541        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1542
1543        self.body = str({"accountId": self.accountId})
1544        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1545        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1546
1547        if self.moreDebug:
1548            uLogger.debug("Records about current open positions successfully received")
1549
1550        return rawPositions
1551
1552    def RequestPendingOrders(self) -> list:
1553        """
1554        Requesting current actual pending limit orders for current `accountId`.
1555
1556        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1557
1558        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1559
1560        :return: list of dictionaries with pending limit orders.
1561        """
1562        if self.accountId is None or not self.accountId:
1563            uLogger.error("Variable `accountId` must be defined for using this method!")
1564            raise Exception("Account ID required")
1565
1566        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1567
1568        self.body = str({"accountId": self.accountId})
1569        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1570        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1571
1572        if "orders" in rawResponse.keys():
1573            rawOrders = rawResponse["orders"]
1574            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1575
1576        else:
1577            rawOrders = []
1578            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1579
1580        return rawOrders
1581
1582    def RequestStopOrders(self) -> list:
1583        """
1584        Requesting current actual stop orders for current `accountId`.
1585
1586        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1587
1588        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1589
1590        :return: list of dictionaries with stop orders.
1591        """
1592        if self.accountId is None or not self.accountId:
1593            uLogger.error("Variable `accountId` must be defined for using this method!")
1594            raise Exception("Account ID required")
1595
1596        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1597
1598        self.body = str({"accountId": self.accountId})
1599        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1600        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1601
1602        if "stopOrders" in rawResponse.keys():
1603            rawStopOrders = rawResponse["stopOrders"]
1604            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1605
1606        else:
1607            rawStopOrders = []
1608            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1609
1610        return rawStopOrders
1611
1612    def Overview(self, show: bool = False, details: str = "full") -> dict:
1613        """
1614        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1615        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1616        and `overviewBondsCalendarFile` are defined then also save information to file.
1617
1618        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1619        many requests about the state of the portfolio, and then, based on the received data, a large number
1620        of calculation and statistics are collected.
1621
1622        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1623        :param details: how detailed should the information be?
1624        - `full` — shows full available information about portfolio status (by default),
1625        - `positions` — shows only open positions,
1626        - `orders` — shows only sections of open limits and stop orders.
1627        - `digest` — show a short digest of the portfolio status,
1628        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1629        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1630        :return: dictionary with client's raw portfolio and some statistics.
1631        """
1632        if self.accountId is None or not self.accountId:
1633            uLogger.error("Variable `accountId` must be defined for using this method!")
1634            raise Exception("Account ID required")
1635
1636        view = {
1637            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1638                "headers": {},  # list of dictionaries, response headers without "positions" section
1639                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1640                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1641                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1642                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1643                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1644                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1645                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1646                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1647                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1648            },
1649            "stat": {  # --- some statistics calculated using "raw" sections:
1650                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1651                "availableRUB": 0.,  # available rubles (without other currencies)
1652                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1653                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1654                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1655                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1656                "sharesCostRUB": 0.,  # costs of all shares in RUB
1657                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1658                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1659                "futuresCostRUB": 0.,  # costs of all futures in RUB
1660                "Currencies": [],  # list of dictionaries of all currencies statistics
1661                "Shares": [],  # list of dictionaries of all shares statistics
1662                "Bonds": [],  # list of dictionaries of all bonds statistics
1663                "Etfs": [],  # list of dictionaries of all etfs statistics
1664                "Futures": [],  # list of dictionaries of all futures statistics
1665                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1666                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1667                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1668                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1669                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1670            },
1671            "analytics": {  # --- some analytics of portfolio:
1672                "distrByAssets": {},  # portfolio distribution by assets
1673                "distrByCompanies": {},  # portfolio distribution by companies
1674                "distrBySectors": {},  # portfolio distribution by sectors
1675                "distrByCurrencies": {},  # portfolio distribution by currencies
1676                "distrByCountries": {},  # portfolio distribution by countries
1677                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1678            }
1679        }
1680
1681        details = details.lower()
1682        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1683        if details not in availableDetails:
1684            details = "full"
1685            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1686
1687        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1688
1689        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1690        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1691        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1692        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1693
1694        # save response headers without "positions" section:
1695        for key in portfolioResponse.keys():
1696            if key != "positions":
1697                view["raw"]["headers"][key] = portfolioResponse[key]
1698
1699            else:
1700                continue
1701
1702        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1703        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1704        for item in portfolioResponse["positions"]:
1705            if item["instrumentType"] == "currency":
1706                self._figi = item["figi"]
1707                curr = self.SearchByFIGI(requestPrice=False)
1708
1709                # current price of currency in RUB:
1710                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1711                    "name": curr["name"],
1712                    "currentPrice": NanoToFloat(
1713                        item["currentPrice"]["units"],
1714                        item["currentPrice"]["nano"]
1715                    ),
1716                }
1717
1718                view["raw"]["Currencies"].append(item)
1719
1720            elif item["instrumentType"] == "share":
1721                view["raw"]["Shares"].append(item)
1722
1723            elif item["instrumentType"] == "bond":
1724                view["raw"]["Bonds"].append(item)
1725
1726            elif item["instrumentType"] == "etf":
1727                view["raw"]["Etfs"].append(item)
1728
1729            elif item["instrumentType"] == "futures":
1730                view["raw"]["Futures"].append(item)
1731
1732            else:
1733                continue
1734
1735        # how many volume of currencies (by ISO currency name) are blocked:
1736        for item in view["raw"]["positions"]["blocked"]:
1737            blocked = NanoToFloat(item["units"], item["nano"])
1738            if blocked > 0:
1739                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1740
1741        # how many volume of instruments (by FIGI) are blocked:
1742        for item in view["raw"]["positions"]["securities"]:
1743            blocked = int(item["blocked"])
1744            if blocked > 0:
1745                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1746
1747        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1748
1749        if "rub" in allBlocked.keys():
1750            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1751
1752        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1753        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1754        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1755        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1756        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1757        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1758        view["stat"]["portfolioCostRUB"] = sum([
1759            view["stat"]["allCurrenciesCostRUB"],
1760            view["stat"]["sharesCostRUB"],
1761            view["stat"]["bondsCostRUB"],
1762            view["stat"]["etfsCostRUB"],
1763            view["stat"]["futuresCostRUB"],
1764        ])
1765
1766        # --- calculating some portfolio statistics:
1767        byComp = {}  # distribution by companies
1768        bySect = {}  # distribution by sectors
1769        byCurr = {}  # distribution by currencies (include RUB)
1770        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1771        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1772
1773        for item in portfolioResponse["positions"]:
1774            self._figi = item["figi"]
1775            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1776
1777            if instrument:
1778                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1779                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1780
1781                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1782                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1783
1784                else:
1785                    blocked = 0
1786
1787                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1788                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1789                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1790                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1791                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1792                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1793                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1794                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1795                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1796                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1797                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1798                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1799
1800                statData = {
1801                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1802                    "ticker": instrument["ticker"],  # ticker by FIGI
1803                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1804                    "volume": volume,  # available volume of instrument
1805                    "lots": lots,  # volume in lots of instrument
1806                    "direction": direction,  # direction of an instrument's position: short or long
1807                    "blocked": blocked,  # blocked volume of currency or instrument
1808                    "currentPrice": curPrice,  # current instrument's price in basic asset
1809                    "average": average,  # current average position price
1810                    "cost": cost,  # current cost of all volume of instrument in basic asset
1811                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1812                    "costRUB": costRUB,  # cost of instrument in ruble
1813                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1814                    "profit": profit,  # expected profit at current moment
1815                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1816                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1817                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1818                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1819                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1820                    "step": instrument["step"],  # minimum price increment
1821                }
1822
1823                # adding distribution by unique countries:
1824                if statData["country"] not in byCountry.keys():
1825                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1826
1827                else:
1828                    byCountry[statData["country"]]["cost"] += costRUB
1829                    byCountry[statData["country"]]["percent"] += percentCostRUB
1830
1831                if item["instrumentType"] != "currency":
1832                    # adding distribution by unique companies:
1833                    if statData["name"]:
1834                        if statData["name"] not in byComp.keys():
1835                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1836
1837                        else:
1838                            byComp[statData["name"]]["cost"] += costRUB
1839                            byComp[statData["name"]]["percent"] += percentCostRUB
1840
1841                    # adding distribution by unique sectors:
1842                    if statData["sector"] not in bySect.keys():
1843                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1844
1845                    else:
1846                        bySect[statData["sector"]]["cost"] += costRUB
1847                        bySect[statData["sector"]]["percent"] += percentCostRUB
1848
1849                # adding distribution by unique currencies:
1850                if currency not in byCurr.keys():
1851                    byCurr[currency] = {
1852                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1853                        "cost": costRUB,
1854                        "percent": percentCostRUB
1855                    }
1856
1857                else:
1858                    byCurr[currency]["cost"] += costRUB
1859                    byCurr[currency]["percent"] += percentCostRUB
1860
1861                # saving statistics for every instrument:
1862                if item["instrumentType"] == "currency":
1863                    view["stat"]["Currencies"].append(statData)
1864
1865                    # update dict with free funds for trading (total - blocked) by currencies
1866                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1867                    view["stat"]["funds"][currency] = {
1868                        "total": volume,
1869                        "totalCostRUB": costRUB,  # total volume cost in rubles
1870                        "free": volume - blocked,
1871                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1872                    }
1873
1874                elif item["instrumentType"] == "share":
1875                    view["stat"]["Shares"].append(statData)
1876
1877                elif item["instrumentType"] == "bond":
1878                    view["stat"]["Bonds"].append(statData)
1879
1880                elif item["instrumentType"] == "etf":
1881                    view["stat"]["Etfs"].append(statData)
1882
1883                elif item["instrumentType"] == "Futures":
1884                    view["stat"]["Futures"].append(statData)
1885
1886                else:
1887                    continue
1888
1889        # total changes in Russian Ruble:
1890        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1891        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1892        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1893        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1894        view["stat"]["funds"]["rub"] = {
1895            "total": view["stat"]["availableRUB"],
1896            "totalCostRUB": view["stat"]["availableRUB"],
1897            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1898            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1899        }
1900
1901        # --- pending limit orders sector data:
1902        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1903        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1904
1905        for item in view["raw"]["orders"]:
1906            self._figi = item["figi"]
1907
1908            if item["figi"] not in uniquePendingOrdersFIGIs:
1909                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1910
1911                uniquePendingOrdersFIGIs.append(item["figi"])
1912                uniquePendingOrders[item["figi"]] = instrument
1913
1914            else:
1915                instrument = uniquePendingOrders[item["figi"]]
1916
1917            if instrument:
1918                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1919                orderType = TKS_ORDER_TYPES[item["orderType"]]
1920                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1921                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1922
1923                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1924                if item["direction"] == "ORDER_DIRECTION_BUY":
1925                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1926
1927                else:
1928                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1929
1930                # requested price for order execution:
1931                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1932
1933                # necessary changes in percent to reach target from current price:
1934                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1935
1936                view["stat"]["orders"].append({
1937                    "orderID": item["orderId"],  # orderId number parameter of current order
1938                    "figi": item["figi"],  # FIGI identification
1939                    "ticker": instrument["ticker"],  # ticker name by FIGI
1940                    "lotsRequested": item["lotsRequested"],  # requested lots value
1941                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1942                    "currentPrice": lastPrice,  # current instrument's price for defined action
1943                    "targetPrice": target,  # requested price for order execution in base currency
1944                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1945                    "percentChanges": changes,  # changes in percent to target from current price
1946                    "currency": item["currency"],  # instrument's currency name
1947                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1948                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1949                    "status": orderState,  # order status from TKS_ORDER_STATES
1950                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1951                })
1952
1953        # --- stop orders sector data:
1954        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1955        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1956
1957        for item in view["raw"]["stopOrders"]:
1958            self._figi = item["figi"]
1959
1960            if item["figi"] not in uniqueStopOrdersFIGIs:
1961                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1962
1963                uniqueStopOrdersFIGIs.append(item["figi"])
1964                uniqueStopOrders[item["figi"]] = instrument
1965
1966            else:
1967                instrument = uniqueStopOrders[item["figi"]]
1968
1969            if instrument:
1970                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1971                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1972                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1973
1974                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1975                if "expirationTime" in item.keys():
1976                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1977                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1978
1979                else:
1980                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1981                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1982
1983                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1984                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1985                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1986
1987                else:
1988                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1989
1990                # requested price when stop-order executed:
1991                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1992
1993                # price for limit-order, set up when stop-order executed:
1994                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1995
1996                # necessary changes in percent to reach target from current price:
1997                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1998
1999                view["stat"]["stopOrders"].append({
2000                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2001                    "figi": item["figi"],  # FIGI identification
2002                    "ticker": instrument["ticker"],  # ticker name by FIGI
2003                    "lotsRequested": item["lotsRequested"],  # requested lots value
2004                    "currentPrice": lastPrice,  # current instrument's price for defined action
2005                    "targetPrice": target,  # requested price for stop-order execution in base currency
2006                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2007                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2008                    "percentChanges": changes,  # changes in percent to target from current price
2009                    "currency": item["currency"],  # instrument's currency name
2010                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2011                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2012                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2013                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2014                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2015                })
2016
2017        # --- calculating data for analytics section:
2018        # portfolio distribution by assets:
2019        view["analytics"]["distrByAssets"] = {
2020            "Ruble": {
2021                "uniques": 1,
2022                "cost": view["stat"]["availableRUB"],
2023                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024            },
2025            "Currencies": {
2026                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2027                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2028                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2029            },
2030            "Shares": {
2031                "uniques": len(view["stat"]["Shares"]),
2032                "cost": view["stat"]["sharesCostRUB"],
2033                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2034            },
2035            "Bonds": {
2036                "uniques": len(view["stat"]["Bonds"]),
2037                "cost": view["stat"]["bondsCostRUB"],
2038                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2039            },
2040            "Etfs": {
2041                "uniques": len(view["stat"]["Etfs"]),
2042                "cost": view["stat"]["etfsCostRUB"],
2043                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2044            },
2045            "Futures": {
2046                "uniques": len(view["stat"]["Futures"]),
2047                "cost": view["stat"]["futuresCostRUB"],
2048                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2049            },
2050        }
2051
2052        # portfolio distribution by companies:
2053        view["analytics"]["distrByCompanies"]["All money cash"] = {
2054            "ticker": "",
2055            "cost": view["stat"]["allCurrenciesCostRUB"],
2056            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2057        }
2058        view["analytics"]["distrByCompanies"].update(byComp)
2059
2060        # portfolio distribution by sectors:
2061        view["analytics"]["distrBySectors"]["All money cash"] = {
2062            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2063            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2064        }
2065        view["analytics"]["distrBySectors"].update(bySect)
2066
2067        # portfolio distribution by currencies:
2068        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2069            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2070
2071            if self.moreDebug:
2072                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2073
2074        view["analytics"]["distrByCurrencies"].update(byCurr)
2075        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2076        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2077
2078        # portfolio distribution by countries:
2079        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2080            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2081
2082            if self.moreDebug:
2083                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2084
2085        view["analytics"]["distrByCountries"].update(byCountry)
2086        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2087        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2088
2089        # --- Prepare text statistics overview in human-readable:
2090        if show:
2091            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2092
2093            # Whatever the value `details`, header not changes:
2094            info = [
2095                "# Client's portfolio\n\n",
2096                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2097                "* **Account ID:** [{}]\n".format(self.accountId),
2098            ]
2099
2100            if details in ["full", "positions", "digest"]:
2101                info.extend([
2102                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2103                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2104                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2105                        view["stat"]["totalChangesRUB"],
2106                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2107                        view["stat"]["totalChangesPercentRUB"],
2108                    ),
2109                ])
2110
2111            if details in ["full", "positions"]:
2112                info.extend([
2113                    "## Open positions\n\n",
2114                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2115                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2116                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2117                        "{:.2f} ({:.2f}) rub".format(
2118                            view["stat"]["availableRUB"],
2119                            view["stat"]["blockedRUB"],
2120                        )
2121                    )
2122                ])
2123
2124                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2125                    return [
2126                        "|                             |                                 |          |              |              |                     |                              |\n",
2127                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2128                            noTradeStr if noTradeStr else typeStr,
2129                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2130                        ),
2131                    ]
2132
2133                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2134                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2135                        "{} [{}]".format(data["ticker"], data["figi"]),
2136                        "{:.2f} ({:.2f}) {}".format(
2137                            data["volume"],
2138                            data["blocked"],
2139                            data["currency"],
2140                        ) if isCurr else "{:.0f} ({:.0f})".format(
2141                            data["volume"],
2142                            data["blocked"],
2143                        ),
2144                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2145                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2146                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2147                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2148                        "{}{:.2f} {} ({}{:.2f}%)".format(
2149                            "+" if data["profit"] > 0 else "",
2150                            data["profit"], data["baseCurrencyName"],
2151                            "+" if data["percentProfit"] > 0 else "",
2152                            data["percentProfit"],
2153                        ),
2154                    )
2155
2156                # --- Show currencies section:
2157                if view["stat"]["Currencies"]:
2158                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2159                    for item in view["stat"]["Currencies"]:
2160                        info.append(_InfoStr(item, isCurr=True))
2161
2162                else:
2163                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2164
2165                # --- Show shares section:
2166                if view["stat"]["Shares"]:
2167                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2168
2169                    for item in view["stat"]["Shares"]:
2170                        info.append(_InfoStr(item))
2171
2172                else:
2173                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2174
2175                # --- Show bonds section:
2176                if view["stat"]["Bonds"]:
2177                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2178
2179                    for item in view["stat"]["Bonds"]:
2180                        info.append(_InfoStr(item))
2181
2182                else:
2183                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2184
2185                # --- Show etfs section:
2186                if view["stat"]["Etfs"]:
2187                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2188
2189                    for item in view["stat"]["Etfs"]:
2190                        info.append(_InfoStr(item))
2191
2192                else:
2193                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2194
2195                # --- Show futures section:
2196                if view["stat"]["Futures"]:
2197                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2198
2199                    for item in view["stat"]["Futures"]:
2200                        info.append(_InfoStr(item))
2201
2202                else:
2203                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2204
2205            if details in ["full", "orders"]:
2206                # --- Show pending limit orders section:
2207                if view["stat"]["orders"]:
2208                    info.extend([
2209                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2210                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2211                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2212                    ])
2213
2214                    for item in view["stat"]["orders"]:
2215                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2216                            "{} [{}]".format(item["ticker"], item["figi"]),
2217                            item["orderID"],
2218                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2219                            "{} {} ({}{:.2f}%)".format(
2220                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2221                                item["baseCurrencyName"],
2222                                "+" if item["percentChanges"] > 0 else "",
2223                                float(item["percentChanges"]),
2224                            ),
2225                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2226                            item["action"],
2227                            item["type"],
2228                            item["date"],
2229                        ))
2230
2231                else:
2232                    info.append("\n## Total pending limit-orders: [0]\n")
2233
2234                # --- Show stop orders section:
2235                if view["stat"]["stopOrders"]:
2236                    info.extend([
2237                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2238                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2239                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2240                    ])
2241
2242                    for item in view["stat"]["stopOrders"]:
2243                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2244                            "{} [{}]".format(item["ticker"], item["figi"]),
2245                            item["orderID"],
2246                            item["lotsRequested"],
2247                            "{} {} ({}{:.2f}%)".format(
2248                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2249                                item["baseCurrencyName"],
2250                                "+" if item["percentChanges"] > 0 else "",
2251                                float(item["percentChanges"]),
2252                            ),
2253                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2254                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2255                            item["action"],
2256                            item["type"],
2257                            item["expType"],
2258                            item["createDate"],
2259                            item["expDate"],
2260                        ))
2261
2262                else:
2263                    info.append("\n## Total stop-orders: [0]\n")
2264
2265            if details in ["full", "analytics"]:
2266                # -- Show analytics section:
2267                if view["stat"]["portfolioCostRUB"] > 0:
2268                    info.extend([
2269                        "\n# Analytics\n\n"
2270                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2271                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2272                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2273                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2274                            view["stat"]["totalChangesRUB"],
2275                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2276                            view["stat"]["totalChangesPercentRUB"],
2277                        ),
2278                        "\n## Portfolio distribution by assets\n"
2279                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2280                        "|------------------------------------|---------|---------|--------------------|\n",
2281                    ])
2282
2283                    for key in view["analytics"]["distrByAssets"].keys():
2284                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2285                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2286                                key,
2287                                view["analytics"]["distrByAssets"][key]["uniques"],
2288                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2289                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2290                            ))
2291
2292                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2293
2294                    info.extend([
2295                        "\n## Portfolio distribution by companies\n"
2296                        "\n| Company                                      | Percent | Current cost       |\n",
2297                        aSepLine,
2298                    ])
2299
2300                    for company in view["analytics"]["distrByCompanies"].keys():
2301                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2302                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2303                                "{}{}".format(
2304                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2305                                    company,
2306                                ),
2307                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2308                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2309                            ))
2310
2311                    info.extend([
2312                        "\n## Portfolio distribution by sectors\n"
2313                        "\n| Sector                                       | Percent | Current cost       |\n",
2314                        aSepLine,
2315                    ])
2316
2317                    for sector in view["analytics"]["distrBySectors"].keys():
2318                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2319                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2320                                sector,
2321                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2322                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2323                            ))
2324
2325                    info.extend([
2326                        "\n## Portfolio distribution by currencies\n"
2327                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2328                        aSepLine,
2329                    ])
2330
2331                    for curr in view["analytics"]["distrByCurrencies"].keys():
2332                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2333                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2334                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2335                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2336                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2337                            ))
2338
2339                    info.extend([
2340                        "\n## Portfolio distribution by countries\n"
2341                        "\n| Assets by country                            | Percent | Current cost       |\n",
2342                        aSepLine,
2343                    ])
2344
2345                    for country in view["analytics"]["distrByCountries"].keys():
2346                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2347                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2348                                country,
2349                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2351                            ))
2352
2353            if details in ["full", "calendar"]:
2354                # -- Show bonds payment calendar section:
2355                if view["stat"]["Bonds"]:
2356                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2357                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2358                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2359
2360                else:
2361                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2362
2363            infoText = "".join(info)
2364
2365            uLogger.info(infoText)
2366
2367            if details == "full" and self.overviewFile:
2368                filename = self.overviewFile
2369
2370            elif details == "digest" and self.overviewDigestFile:
2371                filename = self.overviewDigestFile
2372
2373            elif details == "positions" and self.overviewPositionsFile:
2374                filename = self.overviewPositionsFile
2375
2376            elif details == "orders" and self.overviewOrdersFile:
2377                filename = self.overviewOrdersFile
2378
2379            elif details == "analytics" and self.overviewAnalyticsFile:
2380                filename = self.overviewAnalyticsFile
2381
2382            elif details == "calendar" and self.overviewBondsCalendarFile:
2383                filename = self.overviewBondsCalendarFile
2384
2385            else:
2386                filename = ""
2387
2388            if filename:
2389                with open(filename, "w", encoding="UTF-8") as fH:
2390                    fH.write(infoText)
2391
2392                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2393
2394                if self.useHTMLReports:
2395                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2396                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2397                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2398
2399                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2400
2401        return view
2402
2403    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2404        """
2405        Returns history operations between two given dates for current `accountId`.
2406        If `reportFile` string is not empty then also save human-readable report.
2407        Shows some statistical data of closed positions.
2408
2409        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2410        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2411        :param show: if `True` then also prints all records to the console.
2412        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2413        :return: original list of dictionaries with history of deals records from API ("operations" key):
2414                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2415                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2416        """
2417        if self.accountId is None or not self.accountId:
2418            uLogger.error("Variable `accountId` must be defined for using this method!")
2419            raise Exception("Account ID required")
2420
2421        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2422
2423        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2424
2425        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2426        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2427        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2428        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2429        customStat = {}  # custom statistics in additional to responseJSON
2430
2431        # --- output report in human-readable format:
2432        if show or self.reportFile:
2433            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2434            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2435            nextDay = ""
2436
2437            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2438
2439            if len(ops) > 0:
2440                customStat = {
2441                    "opsCount": 0,  # total operations count
2442                    "buyCount": 0,  # buy operations
2443                    "sellCount": 0,  # sell operations
2444                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2445                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2446                    "payIn": {"rub": 0.},  # Deposit brokerage account
2447                    "payOut": {"rub": 0.},  # Withdrawals
2448                    "divs": {"rub": 0.},  # Dividends income
2449                    "coupons": {"rub": 0.},  # Coupon's income
2450                    "brokerCom": {"rub": 0.},  # Service commissions
2451                    "serviceCom": {"rub": 0.},  # Service commissions
2452                    "marginCom": {"rub": 0.},  # Margin commissions
2453                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2454                }
2455
2456                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2457                for item in ops:
2458                    if item["state"] == "OPERATION_STATE_EXECUTED":
2459                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2460
2461                        # count buy operations:
2462                        if "_BUY" in item["operationType"]:
2463                            customStat["buyCount"] += 1
2464
2465                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2466                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2467
2468                            else:
2469                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2470
2471                        # count sell operations:
2472                        elif "_SELL" in item["operationType"]:
2473                            customStat["sellCount"] += 1
2474
2475                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2476                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2477
2478                            else:
2479                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2480
2481                        # count incoming operations:
2482                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2483                            if item["payment"]["currency"] in customStat["payIn"].keys():
2484                                customStat["payIn"][item["payment"]["currency"]] += payment
2485
2486                            else:
2487                                customStat["payIn"][item["payment"]["currency"]] = payment
2488
2489                        # count withdrawals operations:
2490                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2491                            if item["payment"]["currency"] in customStat["payOut"].keys():
2492                                customStat["payOut"][item["payment"]["currency"]] += payment
2493
2494                            else:
2495                                customStat["payOut"][item["payment"]["currency"]] = payment
2496
2497                        # count dividends income:
2498                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2499                            if item["payment"]["currency"] in customStat["divs"].keys():
2500                                customStat["divs"][item["payment"]["currency"]] += payment
2501
2502                            else:
2503                                customStat["divs"][item["payment"]["currency"]] = payment
2504
2505                        # count coupon's income:
2506                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2507                            if item["payment"]["currency"] in customStat["coupons"].keys():
2508                                customStat["coupons"][item["payment"]["currency"]] += payment
2509
2510                            else:
2511                                customStat["coupons"][item["payment"]["currency"]] = payment
2512
2513                        # count broker commissions:
2514                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2515                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2516                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2517
2518                            else:
2519                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2520
2521                        # count service commissions:
2522                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2523                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2524                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2525
2526                            else:
2527                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2528
2529                        # count margin commissions:
2530                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2531                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2532                                customStat["marginCom"][item["payment"]["currency"]] += payment
2533
2534                            else:
2535                                customStat["marginCom"][item["payment"]["currency"]] = payment
2536
2537                        # count withholding taxes:
2538                        elif "_TAX" in item["operationType"]:
2539                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2540                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2541
2542                            else:
2543                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2544
2545                        else:
2546                            continue
2547
2548                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2549
2550                # --- view "Actions" lines:
2551                info.extend([
2552                    "| Report sections            |                               |                              |                      |                        |\n",
2553                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2554                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2555                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2556                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2557                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2558                    ),
2559                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2560                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2561                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2562                    ),
2563                ])
2564
2565                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2566                for key in opsKeys:
2567                    if key == "rub":
2568                        continue
2569
2570                    info.extend([
2571                        "|                            |                               | {:<28} |                      |                        |\n".format(
2572                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2573                        ),
2574                        "|                            |                               | {:<28} |                      |                        |\n".format(
2575                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2576                        ),
2577                    ])
2578
2579                info.append(splitLine1)
2580
2581                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2582                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2583                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2584                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2585                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2586                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2587                    )
2588
2589                # --- view "Payments" lines:
2590                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2591                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2592
2593                for key in paymentsKeys:
2594                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2595
2596                info.append(splitLine1)
2597
2598                # --- view "Commissions and taxes" lines:
2599                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2600                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2601
2602                for key in comKeys:
2603                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2604
2605                info.extend([
2606                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2607                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2608                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2609                ])
2610
2611            else:
2612                info.append("Broker returned no operations during this period\n")
2613
2614            # --- view "Operations" section:
2615            for item in ops:
2616                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2617                    continue
2618
2619                else:
2620                    self._figi = item["figi"] if item["figi"] else ""
2621                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2622                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2623
2624                    # group of deals during one day:
2625                    if nextDay and item["date"].split("T")[0] != nextDay:
2626                        info.append(splitLine2)
2627                        nextDay = ""
2628
2629                    else:
2630                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2631
2632                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2633                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2634                        self._figi if self._figi else "—",
2635                        instrument["ticker"] if instrument else "—",
2636                        instrument["type"] if instrument else "—",
2637                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2638                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2639                        TKS_OPERATION_STATES[item["state"]],
2640                        TKS_OPERATION_TYPES[item["operationType"]],
2641                    ))
2642
2643            infoText = "".join(info)
2644
2645            if show:
2646                if self.moreDebug:
2647                    uLogger.debug("Records about history of a client's operations successfully received")
2648
2649                uLogger.info(infoText)
2650
2651            if self.reportFile:
2652                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2653                    fH.write(infoText)
2654
2655                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2656
2657                if self.useHTMLReports:
2658                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2659                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2660                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2661
2662                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2663
2664        return ops, customStat
2665
2666    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2667        """
2668        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2669
2670        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2671        Warning! Broker server used ISO UTC time by default.
2672
2673        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2674        Also, `historyFile` used to update history with `onlyMissing` parameter.
2675
2676        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2677
2678        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2679        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2680        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2681                         `"hour"`, `"day"`. Default: `"hour"`.
2682        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2683                            False by default. Warning! History appends only from last candle to current time
2684                            with always update last candle!
2685        :param csvSep: separator if csv-file is used, `,` by default.
2686        :param show: if `True` then also prints Pandas DataFrame to the console.
2687        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2688                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2689        """
2690        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2691        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2692        history = None  # empty pandas object for history
2693
2694        if interval not in TKS_CANDLE_INTERVALS.keys():
2695            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2696            raise Exception("Incorrect value")
2697
2698        if not (self._ticker or self._figi):
2699            uLogger.error("Ticker or FIGI must be defined!")
2700            raise Exception("Ticker or FIGI required")
2701
2702        if self._ticker and not self._figi:
2703            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2704            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2705
2706        if self._figi and not self._ticker:
2707            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2708            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2709
2710        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2711        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2712        if interval.lower() != "day":
2713            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2714
2715        delta = dtEnd - dtStart  # current UTC time minus last time in file
2716        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2717
2718        # calculate history length in candles:
2719        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2720        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2721            length += 1  # to avoid fraction time
2722
2723        # calculate data blocks count:
2724        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2725
2726        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2727        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2728        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2729        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2730        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2731
2732        tempOld = None  # pandas object for old history, if --only-missing key present
2733        lastTime = None  # datetime object of last old candle in file
2734
2735        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2736            uLogger.debug("--only-missing key present, add only last missing candles...")
2737            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2738
2739            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2740
2741            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2742            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2743            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2744            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2745
2746            # get last datetime object from last string in file or minus 1 delta if file is empty:
2747            if len(tempOld) > 0:
2748                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2749
2750            else:
2751                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2752
2753            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2754
2755        responseJSONs = []  # raw history blocks of data
2756
2757        blockEnd = dtEnd
2758        for item in range(blocks):
2759            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2760            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2761
2762            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2763                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2764            ))
2765
2766            if blockStart == blockEnd:
2767                uLogger.debug("Skipped this zero-length block...")
2768
2769            else:
2770                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2771                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2772                self.body = str({
2773                    "figi": self._figi,
2774                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2775                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2776                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2777                })
2778                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2779
2780                if "code" in responseJSON.keys():
2781                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2782
2783                else:
2784                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2785                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2786
2787                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2788
2789            blockEnd = blockStart
2790
2791        printCount = len(responseJSONs)  # candles to show in console
2792        if responseJSONs:
2793            tempHistory = pd.DataFrame(
2794                data={
2795                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2796                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2797                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2798                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2799                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2800                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2801                    "volume": [int(item["volume"]) for item in responseJSONs],
2802                },
2803                index=range(len(responseJSONs)),
2804                columns=["date", "time", "open", "high", "low", "close", "volume"],
2805            )
2806            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2807            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2808
2809            # append only newest candles to old history if --only-missing key present:
2810            if onlyMissing and tempOld is not None and lastTime is not None:
2811                index = 0  # find start index in tempHistory data:
2812
2813                for i, item in tempHistory.iterrows():
2814                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2815
2816                    if curTime == lastTime:
2817                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2818                        index = i
2819                        printCount = index + 1
2820                        break
2821
2822                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2823
2824            else:
2825                history = tempHistory  # if no `--only-missing` key then load full data from server
2826
2827            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2828
2829        if history is not None and not history.empty:
2830            if show:
2831                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2832                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2833                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2834                ))
2835
2836        else:
2837            uLogger.warning("Received an empty candles history!")
2838
2839        if self.historyFile is not None:
2840            if history is not None and not history.empty:
2841                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2842                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2843
2844            else:
2845                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2846
2847        else:
2848            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2849
2850        return history
2851
2852    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2853        """
2854        Load candles history from csv-file and return Pandas DataFrame object.
2855
2856        See also: `History()` and `ShowHistoryChart()` methods.
2857
2858        :param filePath: path to csv-file to open.
2859        """
2860        loadedHistory = None  # init candles data object
2861
2862        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2863
2864        if os.path.exists(filePath):
2865            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2866
2867            tfStr = self.priceModel.FormattedDelta(
2868                self.priceModel.timeframe,
2869                "{days} days {hours}h {minutes}m {seconds}s",
2870            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2871                self.priceModel.timeframe,
2872                "{hours}h {minutes}m {seconds}s",
2873            )
2874
2875            if loadedHistory is not None and not loadedHistory.empty:
2876                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2877                    len(loadedHistory),
2878                    tfStr,
2879                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2880                )
2881
2882            else:
2883                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2884
2885        else:
2886            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2887
2888        return loadedHistory
2889
2890    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2891        """
2892        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2893
2894        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2895        Default: `index.html` (both for interact and non-interact candlesticks chart).
2896
2897        See also: `History()` and `LoadHistory()` methods.
2898
2899        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2900        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2901                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2902                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2903                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2904        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2905                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2906        """
2907        if isinstance(candles, str):
2908            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2909            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2910
2911        elif isinstance(candles, pd.DataFrame):
2912            self.priceModel.prices = candles  # set candles chain from variable
2913            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2914
2915            if "datetime" not in candles.columns:
2916                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2917
2918        else:
2919            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2920            raise Exception("Incorrect value")
2921
2922        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2923
2924        if interact:
2925            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2926
2927            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2928
2929        else:
2930            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2931
2932            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2933
2934        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2935
2936    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2937        """
2938        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2939        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2940
2941        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2942
2943        :param operation: string "Buy" or "Sell".
2944        :param lots: volume, integer count of lots >= 1.
2945        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2946        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2947        :param expDate: string "Undefined" by default or local date in future,
2948                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2949        :return: JSON with response from broker server.
2950        """
2951        if self.accountId is None or not self.accountId:
2952            uLogger.error("Variable `accountId` must be defined for using this method!")
2953            raise Exception("Account ID required")
2954
2955        if operation is None or not operation or operation not in ("Buy", "Sell"):
2956            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2957            raise Exception("Incorrect value")
2958
2959        if lots is None or lots < 1:
2960            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2961            lots = 1
2962
2963        if tp is None or tp < 0:
2964            tp = 0
2965
2966        if sl is None or sl < 0:
2967            sl = 0
2968
2969        if expDate is None or not expDate:
2970            expDate = "Undefined"
2971
2972        if not (self._ticker or self._figi):
2973            uLogger.error("Ticker or FIGI must be defined!")
2974            raise Exception("Ticker or FIGI required")
2975
2976        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2977        self._ticker = instrument["ticker"]
2978        self._figi = instrument["figi"]
2979
2980        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2981
2982        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2983        self.body = str({
2984            "figi": self._figi,
2985            "quantity": str(lots),
2986            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2987            "accountId": str(self.accountId),
2988            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2989        })
2990        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2991
2992        if "orderId" in response.keys():
2993            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2994                operation, response["orderId"],
2995                self._ticker, self._figi, lots,
2996                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2997                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2998                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2999            ))
3000
3001            if tp > 0:
3002                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3003
3004            if sl > 0:
3005                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3006
3007        else:
3008            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3009
3010        return response
3011
3012    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3013        """
3014        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3015        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3016
3017        See also: `Order()` and `Trade()` docstrings.
3018
3019        :param lots: volume, integer count of lots >= 1.
3020        :param tp: float > 0, take profit price of stop-order.
3021        :param sl: float > 0, stop loss price of stop-order.
3022        :param expDate: it's a local date in future.
3023                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3024        :return: JSON with response from broker server.
3025        """
3026        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3027
3028    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3029        """
3030        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3031        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3032
3033        See also: `Order()` and `Trade()` docstrings.
3034
3035        :param lots: volume, integer count of lots >= 1.
3036        :param tp: float > 0, take profit price of stop-order.
3037        :param sl: float > 0, stop loss price of stop-order.
3038        :param expDate: it's a local date in the future.
3039                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3040        :return: JSON with response from broker server.
3041        """
3042        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3043
3044    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3045        """
3046        Close position of given instruments.
3047
3048        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3049        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3050                         This avoids unnecessary downloading data from the server.
3051        """
3052        if instruments is None or not instruments:
3053            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3054            raise Exception("Ticker or FIGI required")
3055
3056        if isinstance(instruments, str):
3057            instruments = [instruments]
3058
3059        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3060        if uniqueInstruments:
3061            if portfolio is None or not portfolio:
3062                portfolio = self.Overview(show=False)
3063
3064            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3065            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3066
3067            for self._figi in uniqueInstruments:
3068                if self._figi not in allOpened:
3069                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3070                    continue
3071
3072                # search open trade info about instrument by ticker:
3073                instrument = {}
3074                for iType in TKS_INSTRUMENTS:
3075                    if instrument:
3076                        break
3077
3078                    for item in portfolio["stat"][iType]:
3079                        if item["figi"] == self._figi:
3080                            instrument = item
3081                            break
3082
3083                if instrument:
3084                    self._ticker = instrument["ticker"]
3085                    self._figi = instrument["figi"]
3086
3087                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3088                        self._ticker,
3089                        self._figi,
3090                        int(instrument["volume"]),
3091                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3092                    ))
3093
3094                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3095
3096                    if tradeLots > 0:
3097                        if instrument["blocked"] > 0:
3098                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3099                                instrument["blocked"],
3100                                self._ticker,
3101                                tradeLots,
3102                            ))
3103
3104                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3105                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3106
3107                    else:
3108                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3109
3110    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3111        """
3112        Close all positions of given instruments with defined type.
3113
3114        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3115        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3116                         This avoids unnecessary downloading data from the server.
3117        """
3118        if iType not in TKS_INSTRUMENTS:
3119            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3120
3121        else:
3122            if portfolio is None or not portfolio:
3123                portfolio = self.Overview(show=False)
3124
3125            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3126            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3127
3128            if tickers and portfolio:
3129                self.CloseTrades(tickers, portfolio)
3130
3131            else:
3132                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3133
3134    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3135        """
3136        Universal method to create market or limit orders with all available parameters for current `accountId`.
3137        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3138
3139        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3140        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3141
3142        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3143        then broker immediately open market order as you can do simple --buy or --sell operations!
3144
3145        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3146        When current price will go up or down to target price value then broker opens a limit order.
3147        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3148
3149        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3150
3151        :param operation: string "Buy" or "Sell".
3152        :param orderType: string "Limit" or "Stop".
3153        :param lots: volume, integer count of lots >= 1.
3154        :param targetPrice: target price > 0. This is open trade price for limit order.
3155        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3156                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3157        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3158                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3159                         Stop loss order always executed by market price.
3160        :param expDate: string "Undefined" by default or local date in future.
3161                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3162                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3163                        A limit order has no expiration date, it lasts until the end of the trading day.
3164        :return: JSON with response from broker server.
3165        """
3166        if self.accountId is None or not self.accountId:
3167            uLogger.error("Variable `accountId` must be defined for using this method!")
3168            raise Exception("Account ID required")
3169
3170        if operation is None or not operation or operation not in ("Buy", "Sell"):
3171            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3172            raise Exception("Incorrect value")
3173
3174        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3175            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3176            raise Exception("Incorrect value")
3177
3178        if lots is None or lots < 1:
3179            uLogger.error("You must define trade volume > 0: integer count of lots!")
3180            raise Exception("Incorrect value")
3181
3182        if targetPrice is None or targetPrice <= 0:
3183            uLogger.error("Target price for limit-order must be greater than 0!")
3184            raise Exception("Incorrect value")
3185
3186        if limitPrice is None or limitPrice <= 0:
3187            limitPrice = targetPrice
3188
3189        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3190            stopType = "Limit"
3191
3192        if expDate is None or not expDate:
3193            expDate = "Undefined"
3194
3195        if not (self._ticker or self._figi):
3196            uLogger.error("Tocker or FIGI must be defined!")
3197            raise Exception("Ticker or FIGI required")
3198
3199        response = {}
3200        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3201        self._ticker = instrument["ticker"]
3202        self._figi = instrument["figi"]
3203
3204        if orderType == "Limit":
3205            uLogger.debug(
3206                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3207                    self._ticker, self._figi,
3208                    operation, lots, targetPrice, instrument["currency"],
3209                ))
3210
3211            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3212            self.body = str({
3213                "figi": self._figi,
3214                "quantity": str(lots),
3215                "price": FloatToNano(targetPrice),
3216                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3217                "accountId": str(self.accountId),
3218                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3219            })
3220            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3221
3222            if "orderId" in response.keys():
3223                uLogger.info(
3224                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3225                        response["orderId"], self._ticker, self._figi, operation, lots,
3226                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3227                    ))
3228
3229                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3230                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3231                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3232                            targetPrice, instrument["currency"],
3233                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3234                        ))
3235
3236                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3237                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3238                            targetPrice, instrument["currency"],
3239                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3240                        ))
3241
3242            else:
3243                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3244
3245        if orderType == "Stop":
3246            uLogger.debug(
3247                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3248                    self._ticker, self._figi,
3249                    operation, lots,
3250                    targetPrice, instrument["currency"],
3251                    limitPrice, instrument["currency"],
3252                    stopType, expDate,
3253                ))
3254
3255            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3256            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3257            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3258
3259            body = {
3260                "figi": self._figi,
3261                "quantity": str(lots),
3262                "price": FloatToNano(limitPrice),
3263                "stopPrice": FloatToNano(targetPrice),
3264                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3265                "accountId": str(self.accountId),
3266                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3267                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3268            }
3269
3270            if expDateUTC:
3271                body["expireDate"] = expDateUTC
3272
3273            self.body = str(body)
3274            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3275
3276            if "stopOrderId" in response.keys():
3277                uLogger.info(
3278                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3279                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3280                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3281                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3282                        TKS_STOP_ORDER_TYPES[stopOrderType],
3283                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3284                    ))
3285
3286                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3287                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3288                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3289                            targetPrice, instrument["currency"],
3290                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3291                        ))
3292
3293                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3294                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3295                            targetPrice, instrument["currency"],
3296                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3297                        ))
3298
3299            else:
3300                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3301
3302        return response
3303
3304    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3305        """
3306        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3307        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3308        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3309        See also: `Order()` docstring.
3310
3311        :param lots: volume, integer count of lots >= 1.
3312        :param targetPrice: target price > 0. This is open trade price for limit order.
3313        :return: JSON with response from broker server.
3314        """
3315        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3316
3317    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3318        """
3319        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3320        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3321        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3322        target price value then broker opens a limit order. See also: `Order()` docstring.
3323
3324        :param lots: volume, integer count of lots >= 1.
3325        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3326        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3327                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3328        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3329                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3330        :param expDate: string "Undefined" by default or local date in future.
3331                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3332                        This date is converting to UTC format for server.
3333        :return: JSON with response from broker server.
3334        """
3335        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3336
3337    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3338        """
3339        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3340        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3341        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3342        See also: `Order()` docstring.
3343
3344        :param lots: volume, integer count of lots >= 1.
3345        :param targetPrice: target price > 0. This is open trade price for limit order.
3346        :return: JSON with response from broker server.
3347        """
3348        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3349
3350    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3351        """
3352        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3353        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3354        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3355        target price value then broker opens a limit order. See also: `Order()` docstring.
3356
3357        :param lots: volume, integer count of lots >= 1.
3358        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3359        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3360                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3361        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3362                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3363        :param expDate: string "Undefined" by default or local date in future.
3364                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3365                        This date is converting to UTC format for server.
3366        :return: JSON with response from broker server.
3367        """
3368        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3369
3370    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3371        """
3372        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3373
3374        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3375        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3376                             This avoids unnecessary downloading data from the server.
3377        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3378        """
3379        if self.accountId is None or not self.accountId:
3380            uLogger.error("Variable `accountId` must be defined for using this method!")
3381            raise Exception("Account ID required")
3382
3383        if orderIDs:
3384            if allOrdersIDs is None:
3385                rawOrders = self.RequestPendingOrders()
3386                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3387
3388            if allStopOrdersIDs is None:
3389                rawStopOrders = self.RequestStopOrders()
3390                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3391
3392            for orderID in orderIDs:
3393                idInPendingOrders = orderID in allOrdersIDs
3394                idInStopOrders = orderID in allStopOrdersIDs
3395
3396                if not (idInPendingOrders or idInStopOrders):
3397                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3398                    continue
3399
3400                else:
3401                    if idInPendingOrders:
3402                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3403
3404                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3405                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3406                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3407                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3408
3409                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3410                            if self.moreDebug:
3411                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3412
3413                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3414
3415                        else:
3416                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3417
3418                    elif idInStopOrders:
3419                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3420
3421                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3422                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3423                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3424                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3425
3426                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3427                            if self.moreDebug:
3428                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3429
3430                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3431
3432                        else:
3433                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3434
3435                    else:
3436                        continue
3437
3438    def CloseAllOrders(self) -> None:
3439        """
3440        Gets a list of open pending and stop orders and cancel it all.
3441        """
3442        rawOrders = self.RequestPendingOrders()
3443        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3444        lenOrders = len(allOrdersIDs)
3445
3446        rawStopOrders = self.RequestStopOrders()
3447        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3448        lenSOrders = len(allStopOrdersIDs)
3449
3450        if lenOrders > 0 or lenSOrders > 0:
3451            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3452
3453            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3454
3455        else:
3456            uLogger.info("Orders not found, nothing to cancel.")
3457
3458    def CloseAll(self, *args) -> None:
3459        """
3460        Close all available (not blocked) opened trades and orders.
3461
3462        Also, you can select one or more keywords case-insensitive:
3463        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3464
3465        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3466        """
3467        overview = self.Overview(show=False)  # get all open trades info
3468
3469        if len(args) == 0:
3470            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3471            self.CloseAllOrders()  # close all pending and stop orders
3472
3473            for iType in TKS_INSTRUMENTS:
3474                if iType != "Currencies":
3475                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3476
3477        else:
3478            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3479            lowerArgs = [x.lower() for x in args]
3480
3481            if "orders" in lowerArgs:
3482                self.CloseAllOrders()  # close all pending and stop orders
3483
3484            for iType in TKS_INSTRUMENTS:
3485                if iType.lower() in lowerArgs and iType != "Currencies":
3486                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3487
3488    def CloseAllByTicker(self, instrument: str) -> None:
3489        """
3490        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3491
3492        This method searches opened trade and orders of instrument throw all portfolio and then use
3493        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3494
3495        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3496
3497        :param instrument: string with ticker.
3498        """
3499        if instrument is None or not instrument:
3500            uLogger.error("Ticker name must be defined for using this method!")
3501            raise Exception("Ticker required")
3502
3503        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3504
3505        self._ticker = instrument  # try to set instrument as ticker
3506        self._figi = ""
3507
3508        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3509        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3510
3511        if limitAll and self.IsInLimitOrders(portfolio=overview):
3512            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3513            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3514
3515        if stopAll and self.IsInStopOrders(portfolio=overview):
3516            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3517            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3518
3519        if self.IsInPortfolio(portfolio=overview):
3520            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3521            self.CloseTrades(instruments=[instrument], portfolio=overview)
3522
3523    def CloseAllByFIGI(self, instrument: str) -> None:
3524        """
3525        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3526
3527        This method searches opened trade and orders of instrument throw all portfolio and then use
3528        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3529
3530        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3531
3532        :param instrument: string with FIGI id.
3533        """
3534        if instrument is None or not instrument:
3535            uLogger.error("FIGI id must be defined for using this method!")
3536            raise Exception("FIGI required")
3537
3538        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3539
3540        self._ticker = ""
3541        self._figi = instrument  # try to set instrument as FIGI id
3542
3543        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3544        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3545
3546        if limitAll and self.IsInLimitOrders(portfolio=overview):
3547            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3548            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3549
3550        if stopAll and self.IsInStopOrders(portfolio=overview):
3551            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3552            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3553
3554        if self.IsInPortfolio(portfolio=overview):
3555            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3556            self.CloseTrades(instruments=[instrument], portfolio=overview)
3557
3558    @staticmethod
3559    def ParseOrderParameters(operation, **inputParameters):
3560        """
3561        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3562
3563        :param operation: string "Buy" or "Sell".
3564        :param inputParameters: this is dict of strings that looks like this
3565               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3566               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3567               "prices" key: one or more prices to open limit-orders
3568               Counts of values in lots and prices lists must be equals!
3569        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3570        """
3571        # TODO: update order grid work with api v2
3572        pass
3573        # uLogger.debug("Input parameters: {}".format(inputParameters))
3574        #
3575        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3576        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3577        #     raise Exception("Incorrect value")
3578        #
3579        # if "l" in inputParameters.keys():
3580        #     inputParameters["lots"] = inputParameters.pop("l")
3581        #
3582        # if "p" in inputParameters.keys():
3583        #     inputParameters["prices"] = inputParameters.pop("p")
3584        #
3585        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3586        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3587        #     raise Exception("Incorrect value")
3588        #
3589        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3590        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3591        #
3592        # if len(lots) != len(prices):
3593        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3594        #     raise Exception("Incorrect value")
3595        #
3596        # uLogger.debug("Extracted parameters for orders:")
3597        # uLogger.debug("lots = {}".format(lots))
3598        # uLogger.debug("prices = {}".format(prices))
3599        #
3600        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3601        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3602        # uLogger.debug("Order parameters: {}".format(result))
3603        #
3604        # return result
3605
3606    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3607        """
3608        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3609
3610        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3611        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3612        """
3613        result = False
3614        msg = "Instrument not defined!"
3615
3616        if portfolio is None or not portfolio:
3617            portfolio = self.Overview(show=False)
3618
3619        if self._ticker:
3620            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3621            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3622
3623            for iType in TKS_INSTRUMENTS:
3624                for instrument in portfolio["stat"][iType]:
3625                    if instrument["ticker"] == self._ticker:
3626                        result = True
3627                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3628                        break
3629
3630        elif self._figi:
3631            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3632            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3633
3634            for iType in TKS_INSTRUMENTS:
3635                for instrument in portfolio["stat"][iType]:
3636                    if instrument["figi"] == self._figi:
3637                        result = True
3638                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3639                        break
3640
3641        else:
3642            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3643
3644        uLogger.debug(msg)
3645
3646        return result
3647
3648    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3649        """
3650        Returns instrument from the user's portfolio if it presents there.
3651        Instrument must be defined by `ticker` (highly priority) or `figi`.
3652
3653        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3654        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3655        """
3656        result = None
3657        msg = "Instrument not defined!"
3658
3659        if portfolio is None or not portfolio:
3660            portfolio = self.Overview(show=False)
3661
3662        if self._ticker:
3663            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3664            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3665
3666            for iType in TKS_INSTRUMENTS:
3667                for instrument in portfolio["stat"][iType]:
3668                    if instrument["ticker"] == self._ticker:
3669                        result = instrument
3670                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3671                        break
3672
3673        elif self._figi:
3674            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3675            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3676
3677            for iType in TKS_INSTRUMENTS:
3678                for instrument in portfolio["stat"][iType]:
3679                    if instrument["figi"] == self._figi:
3680                        result = instrument
3681                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3682                        break
3683
3684        else:
3685            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3686
3687        uLogger.debug(msg)
3688
3689        return result
3690
3691    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3692        """
3693        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3694
3695        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3696
3697        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3698        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3699        """
3700        result = False
3701        msg = "Instrument not defined!"
3702
3703        if portfolio is None or not portfolio:
3704            portfolio = self.Overview(show=False)
3705
3706        if self._ticker:
3707            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3708            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3709
3710            for instrument in portfolio["stat"]["orders"]:
3711                if instrument["ticker"] == self._ticker:
3712                    result = True
3713                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3714                    break
3715
3716        elif self._figi:
3717            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3718            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3719
3720            for instrument in portfolio["stat"]["orders"]:
3721                if instrument["figi"] == self._figi:
3722                    result = True
3723                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3724                    break
3725
3726        else:
3727            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3728
3729        uLogger.debug(msg)
3730
3731        return result
3732
3733    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3734        """
3735        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3736        Instrument must be defined by `ticker` (highly priority) or `figi`.
3737
3738        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3739
3740        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3741        :return: list with `orderID`s of limit orders.
3742        """
3743        result = []
3744        msg = "Instrument not defined!"
3745
3746        if portfolio is None or not portfolio:
3747            portfolio = self.Overview(show=False)
3748
3749        if self._ticker:
3750            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3751            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3752
3753            for instrument in portfolio["stat"]["orders"]:
3754                if instrument["ticker"] == self._ticker:
3755                    result.append(instrument["orderID"])
3756
3757            if result:
3758                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3759
3760        elif self._figi:
3761            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3762            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3763
3764            for instrument in portfolio["stat"]["orders"]:
3765                if instrument["figi"] == self._figi:
3766                    result.append(instrument["orderID"])
3767
3768            if result:
3769                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3770
3771        else:
3772            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3773
3774        uLogger.debug(msg)
3775
3776        return result
3777
3778    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3779        """
3780        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3781
3782        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3783
3784        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3785        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3786        """
3787        result = False
3788        msg = "Instrument not defined!"
3789
3790        if portfolio is None or not portfolio:
3791            portfolio = self.Overview(show=False)
3792
3793        if self._ticker:
3794            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3795            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3796
3797            for instrument in portfolio["stat"]["stopOrders"]:
3798                if instrument["ticker"] == self._ticker:
3799                    result = True
3800                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3801                    break
3802
3803        elif self._figi:
3804            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3805            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3806
3807            for instrument in portfolio["stat"]["stopOrders"]:
3808                if instrument["figi"] == self._figi:
3809                    result = True
3810                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3811                    break
3812
3813        else:
3814            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3815
3816        uLogger.debug(msg)
3817
3818        return result
3819
3820    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3821        """
3822        Returns list with all `orderID`s of opened stop orders for the instrument.
3823        Instrument must be defined by `ticker` (highly priority) or `figi`.
3824
3825        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3826
3827        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3828        :return: list with `orderID`s of stop orders.
3829        """
3830        result = []
3831        msg = "Instrument not defined!"
3832
3833        if portfolio is None or not portfolio:
3834            portfolio = self.Overview(show=False)
3835
3836        if self._ticker:
3837            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3838            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3839
3840            for instrument in portfolio["stat"]["stopOrders"]:
3841                if instrument["ticker"] == self._ticker:
3842                    result.append(instrument["orderID"])
3843
3844            if result:
3845                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3846
3847        elif self._figi:
3848            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3849            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3850
3851            for instrument in portfolio["stat"]["stopOrders"]:
3852                if instrument["figi"] == self._figi:
3853                    result.append(instrument["orderID"])
3854
3855            if result:
3856                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3857
3858        else:
3859            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3860
3861        uLogger.debug(msg)
3862
3863        return result
3864
3865    def RequestLimits(self) -> dict:
3866        """
3867        Method for obtaining the available funds for withdrawal for current `accountId`.
3868
3869        See also:
3870        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3871        - `OverviewLimits()` method
3872
3873        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3874                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3875                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3876                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3877        """
3878        if self.accountId is None or not self.accountId:
3879            uLogger.error("Variable `accountId` must be defined for using this method!")
3880            raise Exception("Account ID required")
3881
3882        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3883
3884        self.body = str({"accountId": self.accountId})
3885        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3886        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3887
3888        if self.moreDebug:
3889            uLogger.debug("Records about available funds for withdrawal successfully received")
3890
3891        return rawLimits
3892
3893    def OverviewLimits(self, show: bool = False) -> dict:
3894        """
3895        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3896
3897        See also: `RequestLimits()`.
3898
3899        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3900        :return: dict with raw parsed data from server and some calculated statistics about it.
3901        """
3902        if self.accountId is None or not self.accountId:
3903            uLogger.error("Variable `accountId` must be defined for using this method!")
3904            raise Exception("Account ID required")
3905
3906        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3907
3908        view = {
3909            "rawLimits": rawLimits,
3910            "limits": {  # parsed data for every currency:
3911                "money": {  # this is an array of portfolio currency positions
3912                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3913                },
3914                "blocked": {  # this is an array of blocked currency
3915                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3916                },
3917                "blockedGuarantee": {  # this is locked money under collateral for futures
3918                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3919                },
3920            },
3921        }
3922
3923        # --- Prepare text table with limits in human-readable format:
3924        if show:
3925            info = [
3926                "# Withdrawal limits\n\n",
3927                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3928                "* **Account ID:** [{}]\n".format(self.accountId),
3929            ]
3930
3931            if view["limits"]["money"]:
3932                info.extend([
3933                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3934                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3935                ])
3936
3937            else:
3938                info.append("\nNo withdrawal limits\n")
3939
3940            for curr in view["limits"]["money"].keys():
3941                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3942                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3943                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3944
3945                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3946                    "[{}]".format(curr),
3947                    "{:.2f}".format(view["limits"]["money"][curr]),
3948                    "{:.2f}".format(availableMoney),
3949                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3950                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3951                )
3952
3953                if curr == "rub":
3954                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3955
3956                else:
3957                    info.append(infoStr)
3958
3959            infoText = "".join(info)
3960
3961            uLogger.info(infoText)
3962
3963            if self.withdrawalLimitsFile:
3964                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3965                    fH.write(infoText)
3966
3967                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3968
3969                if self.useHTMLReports:
3970                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3971                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3972                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3973
3974                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3975
3976        return view
3977
3978    def RequestAccounts(self) -> dict:
3979        """
3980        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3981
3982        See also:
3983        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3984        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3985        - `OverviewUserInfo()` method
3986
3987        :return: dict with raw data from server that contains accounts info. Example of dict:
3988                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3989                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3990                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3991                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3992        """
3993        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3994
3995        self.body = str({})
3996        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3997        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3998
3999        if self.moreDebug:
4000            uLogger.debug("Records about available accounts successfully received")
4001
4002        return rawAccounts
4003
4004    def RequestUserInfo(self) -> dict:
4005        """
4006        Method for requesting common user's information.
4007
4008        See also:
4009        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4010        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4011        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4012        - `OverviewUserInfo()` method
4013
4014        :return: dict with raw data from server that contains user's information. Example of dict:
4015                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4016                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4017        """
4018        uLogger.debug("Requesting common user's information. Wait, please...")
4019
4020        self.body = str({})
4021        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4022        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4023
4024        if self.moreDebug:
4025            uLogger.debug("Records about current user successfully received")
4026
4027        return rawUserInfo
4028
4029    def RequestMarginStatus(self, accountId: str = None) -> dict:
4030        """
4031        Method for requesting margin calculation for defined account ID.
4032
4033        See also:
4034        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4035        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4036        - `OverviewUserInfo()` method
4037
4038        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4039        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4040                 Example of responses:
4041                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4042                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4043                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4044                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4045                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4046                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4047        """
4048        if accountId is None or not accountId:
4049            if self.accountId is None or not self.accountId:
4050                uLogger.error("Variable `accountId` must be defined for using this method!")
4051                raise Exception("Account ID required")
4052
4053            else:
4054                accountId = self.accountId  # use `self.accountId` (main ID) by default
4055
4056        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4057
4058        self.body = str({"accountId": accountId})
4059        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4060        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4061
4062        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4063            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4064            rawMargin = {}
4065
4066        else:
4067            if self.moreDebug:
4068                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4069
4070        return rawMargin
4071
4072    def RequestTariffLimits(self) -> dict:
4073        """
4074        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4075
4076        See also:
4077        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4078        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4079        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4080        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4081        - `OverviewUserInfo()` method
4082
4083        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4084                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4085                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4086        """
4087        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4088
4089        self.body = str({})
4090        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4091        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4092
4093        if self.moreDebug:
4094            uLogger.debug("Records with limits of current tariff successfully received")
4095
4096        return rawTariffLimits
4097
4098    def RequestBondCoupons(self, iJSON: dict) -> dict:
4099        """
4100        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4101        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4102        All dates are in UTC timezone.
4103
4104        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4105        Documentation:
4106        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4107        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4108
4109        See also: `ExtendBondsData()`.
4110
4111        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4112                      If raw iJSON is not data of bond then server returns an error [400] with message:
4113                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4114        :return: dictionary with bond payment calendar. Response example
4115                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4116                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4117                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4118                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4119        """
4120        if iJSON["figi"] is None or not iJSON["figi"]:
4121            uLogger.error("FIGI must be defined for using this method!")
4122            raise Exception("FIGI required")
4123
4124        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4125        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4126
4127        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4128            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4129            self._figi,
4130            startDate,
4131            endDate,
4132        ))
4133
4134        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4135        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4136        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4137
4138        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4139            uLogger.warning("Instrument type is not bond!")
4140
4141        else:
4142            if self.moreDebug:
4143                uLogger.debug("Records about bond payment calendar successfully received")
4144
4145        return calendar
4146
4147    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4148        """
4149        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4150        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4151        coupon yields, current yields and some statistics etc.
4152
4153        WARNING! This is too long operation if a lot of bonds requested from broker server.
4154
4155        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4156
4157        :param instruments: list of strings with tickers or FIGIs.
4158        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4159                     for further used by data scientists or stock analytics.
4160        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4161                 In XLSX-file and Pandas DataFrame fields mean:
4162                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4163                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4164        """
4165        if instruments is None or not instruments:
4166            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4167            raise Exception("Ticker or FIGI required")
4168
4169        if isinstance(instruments, str):
4170            instruments = [instruments]
4171
4172        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4173
4174        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4175
4176        iCount = len(uniqueInstruments)
4177        tooLong = iCount >= 20
4178        if tooLong:
4179            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4180
4181        bonds = None
4182        for i, self._figi in enumerate(uniqueInstruments):
4183            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4184
4185            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4186                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4187                rawBond = self.SearchByFIGI(requestPrice=True)
4188
4189                # Widen raw data with UTC current time (iData["actualDateTime"]):
4190                actualDate = datetime.now(tzutc())
4191                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4192
4193                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4194                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4195
4196                # Replace some values with human-readable:
4197                iData["nominalCurrency"] = iData["nominal"]["currency"]
4198                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4199                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4200                iData["aciCurrency"] = iData["aciValue"]["currency"]
4201                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4202                iData["issueSize"] = int(iData["issueSize"])
4203                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4204                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4205                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4206                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4207                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4208                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4209                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4210                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4211                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4212                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4213
4214                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4215                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4216                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4217                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4218                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4219                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4220                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4221                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4222                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4223                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4224                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4225
4226                # Widen raw data with calendar data from `rawCalendar` values:
4227                calendarData = []
4228                if "events" in iData["rawCalendar"].keys():
4229                    for item in iData["rawCalendar"]["events"]:
4230                        calendarData.append({
4231                            "couponDate": item["couponDate"],
4232                            "couponNumber": int(item["couponNumber"]),
4233                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4234                            "payCurrency": item["payOneBond"]["currency"],
4235                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4236                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4237                            "couponStartDate": item["couponStartDate"],
4238                            "couponEndDate": item["couponEndDate"],
4239                            "couponPeriod": item["couponPeriod"],
4240                        })
4241
4242                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4243                    if "maturityDate" not in iData.keys():
4244                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4245
4246                # Widen raw data with Coupon Rate.
4247                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4248                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4249                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4250                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4251
4252                # Widen raw data with Yield to Maturity (YTM) on current date.
4253                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4254                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4255                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4256                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4257                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4258                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4259
4260                iData["calendar"] = calendarData  # adds calendar at the end
4261
4262                # Remove not used data:
4263                iData.pop("uid")
4264                iData.pop("positionUid")
4265                iData.pop("currentPrice")
4266                iData.pop("rawCalendar")
4267
4268                colNames = list(iData.keys())
4269                if bonds is None:
4270                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4271
4272                else:
4273                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4274
4275            else:
4276                uLogger.warning("Instrument is not a bond!")
4277
4278            processed = round(100 * (i + 1) / iCount, 1)
4279            if tooLong and processed % 5 == 0:
4280                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4281
4282            else:
4283                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4284
4285        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4286
4287        # Saving bonds from Pandas DataFrame to XLSX sheet:
4288        if xlsx and self.bondsXLSXFile:
4289            with pd.ExcelWriter(
4290                    path=self.bondsXLSXFile,
4291                    date_format=TKS_DATE_FORMAT,
4292                    datetime_format=TKS_DATE_TIME_FORMAT,
4293                    mode="w",
4294            ) as writer:
4295                bonds.to_excel(
4296                    writer,
4297                    sheet_name="Extended bonds data",
4298                    index=True,
4299                    encoding="UTF-8",
4300                    freeze_panes=(1, 1),
4301                )  # saving as XLSX-file with freeze first row and column as headers
4302
4303            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4304
4305        return bonds
4306
4307    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4308        """
4309        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4310
4311        WARNING! This is too long operation if a lot of bonds requested from broker server.
4312
4313        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4314
4315        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4316                        extended information about bonds: main info, current prices, bond payment calendar,
4317                        coupon yields, current yields and some statistics etc.
4318                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4319        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4320                     for further used by data scientists or stock analytics.
4321        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4322        """
4323        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4324            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4325
4326        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4327
4328        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4329        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4330        calendar = None
4331        for bond in extBonds.iterrows():
4332            for item in bond[1]["calendar"]:
4333                cData = {
4334                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4335                    "couponDate": item["couponDate"],
4336                    "figi": bond[1]["figi"],
4337                    "ticker": bond[1]["ticker"],
4338                    "name": bond[1]["name"],
4339                    "couponNumber": item["couponNumber"],
4340                    "payOneBond": item["payOneBond"],
4341                    "payCurrency": item["payCurrency"],
4342                    "couponType": item["couponType"],
4343                    "couponPeriod": item["couponPeriod"],
4344                    "fixDate": item["fixDate"],
4345                    "couponStartDate": item["couponStartDate"],
4346                    "couponEndDate": item["couponEndDate"],
4347                }
4348
4349                if calendar is None:
4350                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4351
4352                else:
4353                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4354
4355        if calendar is not None:
4356            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4357
4358            # Saving calendar from Pandas DataFrame to XLSX sheet:
4359            if xlsx:
4360                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4361
4362                with pd.ExcelWriter(
4363                        path=xlsxCalendarFile,
4364                        date_format=TKS_DATE_FORMAT,
4365                        datetime_format=TKS_DATE_TIME_FORMAT,
4366                        mode="w",
4367                ) as writer:
4368                    humanReadable = calendar.copy(deep=True)
4369                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4370                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4371                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4372                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4373                    humanReadable.columns = colNames  # human-readable column names
4374
4375                    humanReadable.to_excel(
4376                        writer,
4377                        sheet_name="Bond payments calendar",
4378                        index=False,
4379                        encoding="UTF-8",
4380                        freeze_panes=(1, 2),
4381                    )  # saving as XLSX-file with freeze first row and column as headers
4382
4383                    del humanReadable  # release df in memory
4384
4385                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4386
4387        return calendar
4388
4389    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4390        """
4391        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4392        Also, creates Markdown file with calendar data, `calendar.md` by default.
4393
4394        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4395
4396        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4397                        extended information about bonds: main info, current prices, bond payment calendar,
4398                        coupon yields, current yields and some statistics etc.
4399                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4400        :param show: if `True` then also printing bonds payment calendar to the console,
4401                     otherwise save to file `calendarFile` only. `False` by default.
4402        :return: multilines text in Markdown format with bonds payment calendar as a table.
4403        """
4404        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4405            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4406
4407        infoText = "# Bond payments calendar\n\n"
4408
4409        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4410
4411        if not (calendar is None or calendar.empty):
4412            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4413
4414            info = [
4415                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4416                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4417                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4418            ]
4419
4420            newMonth = False
4421            notOneBond = calendar["figi"].nunique() > 1
4422            for i, bond in enumerate(calendar.iterrows()):
4423                if newMonth and notOneBond:
4424                    info.append(splitLine)
4425
4426                info.append(
4427                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4428                        "  √" if bond[1]["paid"] else "  —",
4429                        bond[1]["couponDate"].split("T")[0],
4430                        bond[1]["figi"],
4431                        bond[1]["ticker"],
4432                        bond[1]["couponNumber"],
4433                        "{} {}".format(
4434                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4435                            bond[1]["payCurrency"],
4436                        ),
4437                        bond[1]["couponType"],
4438                        bond[1]["couponPeriod"],
4439                        bond[1]["fixDate"].split("T")[0],
4440                    )
4441                )
4442
4443                if i < len(calendar.values) - 1:
4444                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4445                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4446                    newMonth = False if curDate.month == nextDate.month else True
4447
4448                else:
4449                    newMonth = False
4450
4451            infoText += "".join(info)
4452
4453            if show:
4454                uLogger.info("{}".format(infoText))
4455
4456            if self.calendarFile is not None:
4457                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4458                    fH.write(infoText)
4459
4460                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4461
4462                if self.useHTMLReports:
4463                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4464                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4465                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4466
4467                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4468
4469        else:
4470            infoText += "No data\n"
4471
4472        return infoText
4473
4474    def OverviewAccounts(self, show: bool = False) -> dict:
4475        """
4476        Method for parsing and show simple table with all available user accounts.
4477
4478        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4479
4480        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4481        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4482                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4483                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4484                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4485                                                        "closed": "—", "access": "Full access" }, ...}}`
4486        """
4487        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4488
4489        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4490        accounts = {
4491            item["id"]: {
4492                "type": TKS_ACCOUNT_TYPES[item["type"]],
4493                "name": item["name"],
4494                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4495                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4496                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4497                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4498            } for item in rawAccounts["accounts"]
4499        }
4500
4501        # Raw and parsed data with some fields replaced in "stat" section:
4502        view = {
4503            "rawAccounts": rawAccounts,
4504            "stat": accounts,
4505        }
4506
4507        # --- Prepare simple text table with only accounts data in human-readable format:
4508        if show:
4509            info = [
4510                "# User accounts\n\n",
4511                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4512                "| Account ID   | Type                      | Status                    | Name                           |\n",
4513                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4514            ]
4515
4516            for account in view["stat"].keys():
4517                info.extend([
4518                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4519                        account,
4520                        view["stat"][account]["type"],
4521                        view["stat"][account]["status"],
4522                        view["stat"][account]["name"],
4523                    )
4524                ])
4525
4526            infoText = "".join(info)
4527
4528            uLogger.info(infoText)
4529
4530            if self.userAccountsFile:
4531                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4532                    fH.write(infoText)
4533
4534                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4535
4536                if self.useHTMLReports:
4537                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4538                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4539                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4540
4541                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4542
4543        return view
4544
4545    def OverviewUserInfo(self, show: bool = False) -> dict:
4546        """
4547        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4548
4549        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4550
4551        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4552        :return: dict with raw parsed data from server and some calculated statistics about it.
4553        """
4554        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4555        tmpTicker = self._ticker
4556        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4557        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4558        self._ticker = tmpTicker
4559
4560        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4561        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4562        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4563        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4564        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4565        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4566
4567        # This is dict with parsed common user data:
4568        userInfo = {
4569            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4570            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4571            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4572            "tariff": rawUserInfo["tariff"],
4573        }
4574
4575        # This is an array of dict with parsed margin statuses for every account IDs:
4576        margins = {}
4577        for accountId in accounts.keys():
4578            if rawMargins[accountId]:
4579                margins[accountId] = {
4580                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4581                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4582                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4583                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4584                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4585                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4586                    "missing": missing["volume"],
4587                }
4588
4589            else:
4590                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4591
4592        unary = {}  # unary-connection limits
4593        for item in rawTariffLimits["unaryLimits"]:
4594            if item["limitPerMinute"] in unary.keys():
4595                unary[item["limitPerMinute"]].extend(item["methods"])
4596
4597            else:
4598                unary[item["limitPerMinute"]] = item["methods"]
4599
4600        stream = {}  # stream-connection limits
4601        for item in rawTariffLimits["streamLimits"]:
4602            if item["limit"] in stream.keys():
4603                stream[item["limit"]].extend(item["streams"])
4604
4605            else:
4606                stream[item["limit"]] = item["streams"]
4607
4608        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4609        limits = {
4610            "unary": unary,
4611            "stream": stream,
4612        }
4613
4614        # Raw and parsed data as an output result:
4615        view = {
4616            "rawUserInfo": rawUserInfo,
4617            "rawAccounts": rawAccounts,
4618            "rawMargins": rawMargins,
4619            "rawTariffLimits": rawTariffLimits,
4620            "stat": {
4621                "overview": overview,
4622                "userInfo": userInfo,
4623                "accounts": accounts,
4624                "margins": margins,
4625                "limits": limits,
4626            },
4627        }
4628
4629        # --- Prepare text table with user information in human-readable format:
4630        if show:
4631            info = [
4632                "# Full user information\n\n",
4633                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4634                "## Common information\n\n",
4635                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4636                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4637                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4638                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4639                "\n## User accounts\n\n",
4640            ]
4641
4642            for account in view["stat"]["accounts"].keys():
4643                info.extend([
4644                    "### ID: [{}]\n\n".format(account),
4645                    "| Parameters           | Values                                                       |\n",
4646                    "|----------------------|--------------------------------------------------------------|\n",
4647                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4648                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4649                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4650                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4651                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4652                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4653                ])
4654
4655                if margins[account]:
4656                    info.extend([
4657                        "| Margin status:       | Enabled                                                      |\n",
4658                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4659                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4660                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4661                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4662                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4663                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4664                    ])
4665
4666                else:
4667                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4668
4669            info.extend([
4670                "\n## Current user tariff limits\n",
4671                "\n### See also\n",
4672                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4673                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4674                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4675                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4676                "\n### Unary limits\n",
4677            ])
4678
4679            if unary:
4680                for key, values in sorted(unary.items()):
4681                    info.append("\n* Max requests per minute: {}\n".format(key))
4682
4683                    for value in values:
4684                        info.append("  - {}\n".format(value))
4685
4686            else:
4687                info.append("\nNot available\n")
4688
4689            info.append("\n### Stream limits\n")
4690
4691            if stream:
4692                for key, values in sorted(stream.items()):
4693                    info.append("\n* Max stream connections: {}\n".format(key))
4694
4695                    for value in values:
4696                        info.append("  - {}\n".format(value))
4697
4698            else:
4699                info.append("\nNot available\n")
4700
4701            infoText = "".join(info)
4702
4703            uLogger.info(infoText)
4704
4705            if self.userInfoFile:
4706                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4707                    fH.write(infoText)
4708
4709                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4710
4711                if self.useHTMLReports:
4712                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4713                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4714                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4715
4716                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4717
4718        return view
4719
4720
4721class Args:
4722    """
4723    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4724    """
4725    def __init__(self, **kwargs):
4726        self.__dict__.update(kwargs)
4727
4728    def __getattr__(self, item):
4729        return None
4730
4731
4732def ParseArgs():
4733    """This function get and parse command line keys."""
4734    parser = ArgumentParser()  # command-line string parser
4735
4736    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4737    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4738
4739    # --- options:
4740
4741    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4742    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4743    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4744
4745    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4746    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4747
4748    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4749    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4750
4751    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4752    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4753
4754    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4755    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4756    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4757
4758    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4759    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4760
4761    # --- commands:
4762
4763    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4764
4765    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4766    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4767    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4768    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4769    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4770    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4771    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4772    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4773
4774    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4775    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4776    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4777    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4778    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4779    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4780
4781    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4782    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4783    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4784    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4785
4786    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4787    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4788    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4789
4790    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4791    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4792    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4793    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4794    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4795    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4796    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4797
4798    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4799    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4800    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4801    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4802    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4803
4804    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4805    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4806    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4807
4808    cmdArgs = parser.parse_args()
4809    return cmdArgs
4810
4811
4812def Main(**kwargs):
4813    """
4814    Main function for work with TKSBrokerAPI in the console.
4815
4816    See examples:
4817    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4818    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4819    """
4820    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4821
4822    if args.debug_level:
4823        uLogger.level = 10  # always debug level by default
4824        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4825
4826    exitCode = 0
4827    start = datetime.now(tzutc())
4828    uLogger.debug("=-" * 50)
4829    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4830        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4831        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4832    ))
4833
4834    # trying to calculate full current version:
4835    buildVersion = __version__
4836    try:
4837        v = version("tksbrokerapi")
4838        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4839
4840    except Exception:
4841        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4842
4843    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4844    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4845
4846    try:
4847        if args.version:
4848            print("TKSBrokerAPI {}".format(buildVersion))
4849            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4850
4851        else:
4852            # Init class for trading with Tinkoff Broker:
4853            trader = TinkoffBrokerServer(
4854                token=args.token,
4855                accountId=args.account_id,
4856                useCache=not args.no_cache,
4857            )
4858
4859            # --- set some options:
4860
4861            if args.more:
4862                trader.moreDebug = True
4863                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4864
4865            if args.html:
4866                trader.useHTMLReports = True
4867
4868            if args.ticker:
4869                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4870
4871                if ticker in trader.aliasesKeys:
4872                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4873
4874                else:
4875                    trader.ticker = ticker
4876
4877            if args.figi:
4878                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4879
4880            if args.depth is not None:
4881                trader.depth = args.depth
4882
4883            # --- do one command:
4884
4885            if args.list:
4886                if args.output is not None:
4887                    trader.instrumentsFile = args.output
4888
4889                trader.ShowInstrumentsInfo(show=True)
4890
4891            elif args.list_xlsx:
4892                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4893
4894            elif args.bonds_xlsx is not None:
4895                if args.output is not None:
4896                    trader.bondsXLSXFile = args.output
4897
4898                if len(args.bonds_xlsx) == 0:
4899                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4900
4901                else:
4902                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4903
4904            elif args.search:
4905                if args.output is not None:
4906                    trader.searchResultsFile = args.output
4907
4908                trader.SearchInstruments(pattern=args.search[0], show=True)
4909
4910            elif args.info:
4911                if not (args.ticker or args.figi):
4912                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4913                    raise Exception("Ticker or FIGI required")
4914
4915                if args.output is not None:
4916                    trader.infoFile = args.output
4917
4918                if args.ticker:
4919                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4920
4921                else:
4922                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4923
4924            elif args.calendar is not None:
4925                if args.output is not None:
4926                    trader.calendarFile = args.output
4927
4928                if len(args.calendar) == 0:
4929                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4930
4931                else:
4932                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4933
4934                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4935
4936            elif args.price:
4937                if not (args.ticker or args.figi):
4938                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4939                    raise Exception("Ticker or FIGI required")
4940
4941                trader.GetCurrentPrices(show=True)
4942
4943            elif args.prices is not None:
4944                if args.output is not None:
4945                    trader.pricesFile = args.output
4946
4947                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4948
4949            elif args.overview:
4950                if args.output is not None:
4951                    trader.overviewFile = args.output
4952
4953                trader.Overview(show=True, details="full")
4954
4955            elif args.overview_digest:
4956                if args.output is not None:
4957                    trader.overviewDigestFile = args.output
4958
4959                trader.Overview(show=True, details="digest")
4960
4961            elif args.overview_positions:
4962                if args.output is not None:
4963                    trader.overviewPositionsFile = args.output
4964
4965                trader.Overview(show=True, details="positions")
4966
4967            elif args.overview_orders:
4968                if args.output is not None:
4969                    trader.overviewOrdersFile = args.output
4970
4971                trader.Overview(show=True, details="orders")
4972
4973            elif args.overview_analytics:
4974                if args.output is not None:
4975                    trader.overviewAnalyticsFile = args.output
4976
4977                trader.Overview(show=True, details="analytics")
4978
4979            elif args.overview_calendar:
4980                if args.output is not None:
4981                    trader.overviewAnalyticsFile = args.output
4982
4983                trader.Overview(show=True, details="calendar")
4984
4985            elif args.deals is not None:
4986                if args.output is not None:
4987                    trader.reportFile = args.output
4988
4989                if 0 <= len(args.deals) < 3:
4990                    trader.Deals(
4991                        start=args.deals[0] if len(args.deals) >= 1 else None,
4992                        end=args.deals[1] if len(args.deals) == 2 else None,
4993                        show=True,  # Always show deals report in console
4994                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4995                    )
4996
4997                else:
4998                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4999                    raise Exception("Incorrect value")
5000
5001            elif args.history is not None:
5002                if args.output is not None:
5003                    trader.historyFile = args.output
5004
5005                if 0 <= len(args.history) < 3:
5006                    dataReceived = trader.History(
5007                        start=args.history[0] if len(args.history) >= 1 else None,
5008                        end=args.history[1] if len(args.history) == 2 else None,
5009                        interval="hour" if args.interval is None or not args.interval else args.interval,
5010                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5011                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5012                        show=True,  # shows all downloaded candles in console
5013                    )
5014
5015                    if args.render_chart is not None and dataReceived is not None:
5016                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5017
5018                        trader.ShowHistoryChart(
5019                            candles=dataReceived,
5020                            interact=iChart,
5021                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5022                        )
5023
5024                else:
5025                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5026                    raise Exception("Incorrect value")
5027
5028            elif args.load_history is not None:
5029                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5030
5031                if args.render_chart is not None and histData is not None:
5032                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5033                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5034
5035                    trader.ShowHistoryChart(
5036                        candles=histData,
5037                        interact=iChart,
5038                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5039                    )
5040
5041            elif args.trade is not None:
5042                if 1 <= len(args.trade) <= 5:
5043                    trader.Trade(
5044                        operation=args.trade[0],
5045                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5046                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5047                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5048                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5049                    )
5050
5051                else:
5052                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5053
5054            elif args.buy is not None:
5055                if 0 <= len(args.buy) <= 4:
5056                    trader.Buy(
5057                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5058                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5059                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5060                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5061                    )
5062
5063                else:
5064                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5065
5066            elif args.sell is not None:
5067                if 0 <= len(args.sell) <= 4:
5068                    trader.Sell(
5069                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5070                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5071                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5072                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5073                    )
5074
5075                else:
5076                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5077
5078            elif args.order:
5079                if 4 <= len(args.order) <= 7:
5080                    trader.Order(
5081                        operation=args.order[0],
5082                        orderType=args.order[1],
5083                        lots=int(args.order[2]),
5084                        targetPrice=float(args.order[3]),
5085                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5086                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5087                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5088                    )
5089
5090                else:
5091                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5092
5093            elif args.buy_limit:
5094                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5095
5096            elif args.sell_limit:
5097                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5098
5099            elif args.buy_stop:
5100                if 2 <= len(args.buy_stop) <= 7:
5101                    trader.BuyStop(
5102                        lots=int(args.buy_stop[0]),
5103                        targetPrice=float(args.buy_stop[1]),
5104                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5105                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5106                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5107                    )
5108
5109                else:
5110                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5111
5112            elif args.sell_stop:
5113                if 2 <= len(args.sell_stop) <= 7:
5114                    trader.SellStop(
5115                        lots=int(args.sell_stop[0]),
5116                        targetPrice=float(args.sell_stop[1]),
5117                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5118                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5119                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5120                    )
5121
5122                else:
5123                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5124
5125            # elif args.buy_order_grid is not None:
5126            #     # update order grid work with api v2
5127            #     if len(args.buy_order_grid) == 2:
5128            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5129            #
5130            #         for order in orderParams:
5131            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5132            #
5133            #     else:
5134            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5135            #
5136            # elif args.sell_order_grid is not None:
5137            #     # update order grid work with api v2
5138            #     if len(args.sell_order_grid) >= 2:
5139            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5140            #
5141            #         for order in orderParams:
5142            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5143            #
5144            #     else:
5145            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5146
5147            elif args.close_order is not None:
5148                trader.CloseOrders(args.close_order)  # close only one order
5149
5150            elif args.close_orders is not None:
5151                trader.CloseOrders(args.close_orders)  # close list of orders
5152
5153            elif args.close_trade:
5154                if not (args.ticker or args.figi):
5155                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5156                    raise Exception("Ticker or FIGI required")
5157
5158                if args.ticker:
5159                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5160
5161                else:
5162                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5163
5164            elif args.close_trades is not None:
5165                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5166
5167            elif args.close_all is not None:
5168                if args.ticker:
5169                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5170
5171                elif args.figi:
5172                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5173
5174                else:
5175                    trader.CloseAll(*args.close_all)
5176
5177            elif args.limits:
5178                if args.output is not None:
5179                    trader.withdrawalLimitsFile = args.output
5180
5181                trader.OverviewLimits(show=True)
5182
5183            elif args.user_info:
5184                if args.output is not None:
5185                    trader.userInfoFile = args.output
5186
5187                trader.OverviewUserInfo(show=True)
5188
5189            elif args.account:
5190                if args.output is not None:
5191                    trader.userAccountsFile = args.output
5192
5193                trader.OverviewAccounts(show=True)
5194
5195            else:
5196                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5197                raise Exception("There is no command to execute")
5198
5199    except Exception:
5200        trace = tb.format_exc()
5201        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5202            if e in trace:
5203                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5204                break
5205
5206        uLogger.debug(trace)
5207        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5208        exitCode = 255  # an error occurred, must be open a ticket for this issue
5209
5210    finally:
5211        finish = datetime.now(tzutc())
5212
5213        if exitCode == 0:
5214            if args.more:
5215                uLogger.debug("All operations were finished success (summary code is 0).")
5216
5217        else:
5218            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5219                os.path.abspath(uLog.defaultLogFile), exitCode,
5220            ))
5221
5222        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5223        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5224            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5225            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5226        ))
5227        uLogger.debug("=-" * 50)
5228
5229        if not kwargs:
5230            sys.exit(exitCode)
5231
5232        else:
5233            return exitCode
5234
5235
5236if __name__ == "__main__":
5237    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.__lock = Lock()  # initialize multiprocessing mutex lock
 130
 131        self.aliases = TKS_TICKER_ALIASES
 132        """Some aliases instead official tickers.
 133
 134        See also: `TKSEnums.TKS_TICKER_ALIASES`
 135        """
 136
 137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 138
 139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 140
 141        self._ticker = ""
 142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 143
 144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 146
 147        See also: `SearchByTicker()`, `SearchInstruments()`.
 148        """
 149
 150        self._figi = ""
 151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 152
 153        See also: `SearchByFIGI()`, `SearchInstruments()`.
 154        """
 155
 156        self.depth = 1
 157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 158
 159        See also: `GetCurrentPrices()`.
 160        """
 161
 162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 164
 165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 166        """
 167
 168        uLogger.debug("Broker API server: {}".format(self.server))
 169
 170        self.timeout = 15
 171        """Server operations timeout in seconds. Default: `15`.
 172
 173        See also: `SendAPIRequest()`.
 174        """
 175
 176        self.headers = {
 177            "Content-Type": "application/json",
 178            "accept": "application/json",
 179            "Authorization": "Bearer {}".format(self.token),
 180            "x-app-name": "Tim55667757.TKSBrokerAPI",
 181        }
 182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 183
 184        See also: `SendAPIRequest()`.
 185        """
 186
 187        self.body = None
 188        """Request body which send to broker server. Default: `None`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.moreDebug = False
 194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 195
 196        self.useHTMLReports = False
 197        """
 198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 199        
 200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 201        """
 202
 203        self.historyFile = None
 204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 205
 206        See also: `History()`.
 207        """
 208
 209        self.htmlHistoryFile = "index.html"
 210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 211
 212        See also: `ShowHistoryChart()`.
 213        """
 214
 215        self.instrumentsFile = "instruments.md"
 216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 217
 218        See also: `ShowInstrumentsInfo()`.
 219        """
 220
 221        self.searchResultsFile = "search-results.md"
 222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 223
 224        See also: `SearchInstruments()`.
 225        """
 226
 227        self.pricesFile = "prices.md"
 228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 229
 230        See also: `GetListOfPrices()`.
 231        """
 232
 233        self.infoFile = "info.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 237        """
 238
 239        self.bondsXLSXFile = "ext-bonds.xlsx"
 240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 242
 243        See also: `ExtendBondsData()`.
 244        """
 245
 246        self.calendarFile = "calendar.md"
 247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 248        
 249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 250
 251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 252        """
 253
 254        self.overviewFile = "overview.md"
 255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 256
 257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 258        """
 259
 260        self.overviewDigestFile = "overview-digest.md"
 261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 262
 263        See also: `Overview()` with parameter `details="digest"`.
 264        """
 265
 266        self.overviewPositionsFile = "overview-positions.md"
 267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 268
 269        See also: `Overview()` with parameter `details="positions"`.
 270        """
 271
 272        self.overviewOrdersFile = "overview-orders.md"
 273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 274
 275        See also: `Overview()` with parameter `details="orders"`.
 276        """
 277
 278        self.overviewAnalyticsFile = "overview-analytics.md"
 279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 280
 281        See also: `Overview()` with parameter `details="analytics"`.
 282        """
 283
 284        self.overviewBondsCalendarFile = "overview-calendar.md"
 285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 286
 287        See also: `Overview()` with parameter `details="calendar"`.
 288        """
 289
 290        self.reportFile = "deals.md"
 291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 292
 293        See also: `Deals()`.
 294        """
 295
 296        self.withdrawalLimitsFile = "limits.md"
 297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 298
 299        See also: `OverviewLimits()` and `RequestLimits()`.
 300        """
 301
 302        self.userInfoFile = "user-info.md"
 303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 304
 305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 306        """
 307
 308        self.userAccountsFile = "accounts.md"
 309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 310
 311        See also: `OverviewAccounts()`, `RequestAccounts()`.
 312        """
 313
 314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 316
 317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 318
 319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 320        """
 321
 322        self.iList = None  # init iList for raw instruments data
 323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 324        
 325        See also: `Listing()`, `DumpInstruments()`.
 326        """
 327
 328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 329        if useCache:
 330            if os.path.exists(self.iListDumpFile):
 331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 332                curTime = datetime.now(tzutc())
 333
 334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 336
 337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 338
 339                else:
 340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 341
 342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 343                        os.path.abspath(self.iListDumpFile),
 344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 345                    ))
 346
 347            else:
 348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 350
 351        else:
 352            self.iList = self.Listing()  # request new raw instruments data from broker server
 353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 354
 355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 357
 358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 359        """
 360
 361    @property
 362    def ticker(self) -> str:
 363        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        return self._ticker
 371
 372    @ticker.setter
 373    def ticker(self, value):
 374        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 375
 376        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 377        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 378
 379        See also: `SearchByTicker()`, `SearchInstruments()`.
 380        """
 381        self._ticker = str(value).upper()  # Tickers may be upper case only
 382
 383    @property
 384    def figi(self) -> str:
 385        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 386
 387        See also: `SearchByFIGI()`, `SearchInstruments()`.
 388        """
 389        return self._figi
 390
 391    @figi.setter
 392    def figi(self, value):
 393        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 394
 395        See also: `SearchByFIGI()`, `SearchInstruments()`.
 396        """
 397        self._figi = str(value).upper()  # FIGI may be upper case only
 398
 399    def _ParseJSON(self, rawData="{}") -> dict:
 400        """
 401        Parse JSON from response string.
 402
 403        :param rawData: this is a string with JSON-formatted text.
 404        :return: JSON (dictionary), parsed from server response string.
 405        """
 406        responseJSON = json.loads(rawData) if rawData else {}
 407
 408        if self.moreDebug:
 409            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 410
 411        return responseJSON
 412
 413    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 414        """
 415        Send GET or POST request to broker server and receive JSON object.
 416
 417        self.header: must be defining with dictionary of headers.
 418        self.body: if define then used as request body. None by default.
 419        self.timeout: global request timeout, 15 seconds by default.
 420        :param url: url with REST request.
 421        :param reqType: send "GET" or "POST" request. "GET" by default.
 422        :param retry: how many times retry after first request if an 5xx server errors occurred.
 423        :param pause: sleep time in seconds between retries.
 424        :return: response JSON (dictionary) from broker.
 425        """
 426        if reqType.upper() not in ("GET", "POST"):
 427            uLogger.error("You can define request type: `GET` or `POST`!")
 428            raise Exception("Incorrect value")
 429
 430        if self.moreDebug:
 431            uLogger.debug("Request parameters:")
 432            uLogger.debug("    - REST API URL: {}".format(url))
 433            uLogger.debug("    - request type: {}".format(reqType))
 434            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 435            uLogger.debug("    - body:\n{}".format(self.body))
 436
 437        # fast hack to avoid all operations with some tickers/FIGI
 438        responseJSON = {}
 439        oK = True
 440        for item in self.exclude:
 441            if item in url:
 442                if self.moreDebug:
 443                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 444
 445                oK = False
 446                break
 447
 448        if oK:
 449            with self.__lock:  # acquire the mutex lock
 450                counter = 0
 451                response = None
 452                errMsg = ""
 453
 454                while not response and counter <= retry:
 455                    if reqType == "GET":
 456                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 457
 458                    if reqType == "POST":
 459                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 460
 461                    if self.moreDebug:
 462                        uLogger.debug("Response:")
 463                        uLogger.debug("    - status code: {}".format(response.status_code))
 464                        uLogger.debug("    - reason: {}".format(response.reason))
 465                        uLogger.debug("    - body length: {}".format(len(response.text)))
 466                        uLogger.debug("    - headers:\n{}".format(response.headers))
 467
 468                    # Server returns some headers:
 469                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 470                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 471                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 472                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 473                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 474                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 475                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 476                        sleep(rateLimitWait)
 477
 478                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 479                    if 400 <= response.status_code < 500:
 480                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 481                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 482
 483                        if "code" in response.text and "message" in response.text:
 484                            msgDict = self._ParseJSON(rawData=response.text)
 485                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 486
 487                        counter = retry + 1  # do not retry for 4xx errors
 488
 489                    if 500 <= response.status_code < 600:
 490                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 491                        uLogger.debug("    - not oK, {}".format(errMsg))
 492
 493                        if "code" in response.text and "message" in response.text:
 494                            errMsgDict = self._ParseJSON(rawData=response.text)
 495                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 496
 497                        counter += 1
 498
 499                        if counter <= retry:
 500                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 501                            sleep(pause)
 502
 503                responseJSON = self._ParseJSON(rawData=response.text)
 504
 505                if errMsg:
 506                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 507                    uLogger.error("    - not oK, {}".format(errMsg))
 508
 509        return responseJSON
 510
 511    def _IUpdater(self, iType: str) -> tuple:
 512        """
 513        Request instrument by type from server. See available API methods for instruments:
 514        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 515        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 516        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 517        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 518        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 519
 520        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 521        :return: tuple with iType name and list of available instruments of current type for defined user token.
 522        """
 523        result = []
 524
 525        if iType in TKS_INSTRUMENTS:
 526            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 527
 528            # all instruments have the same body in API v2 requests:
 529            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 530            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 531            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 532
 533        return iType, result
 534
 535    def _IWrapper(self, kwargs):
 536        """
 537        Wrapper runs instrument's update method `_IUpdater()`.
 538        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 539        """
 540        return self._IUpdater(**kwargs)
 541
 542    def Listing(self) -> dict:
 543        """
 544        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 545
 546        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 547        """
 548        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 549        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 550
 551        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 552        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 553        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 554
 555        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 556        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 557        poolUpdater.close()  # close the thread pool
 558        poolUpdater.join()  # wait a moment until all data returns from threads
 559
 560        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 561        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 562        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 563
 564        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 565        for iType in iList.keys():
 566            for ticker in iList[iType]:
 567                iList[iType][ticker]["type"] = iType
 568
 569                if "minPriceIncrement" in iList[iType][ticker].keys():
 570                    iList[iType][ticker]["step"] = NanoToFloat(
 571                        iList[iType][ticker]["minPriceIncrement"]["units"],
 572                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 573                    )
 574
 575                else:
 576                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 577
 578        return iList
 579
 580    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 581        """
 582        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 583
 584        See also: `DumpInstruments()`, `Listing()`.
 585
 586        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 587                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 588        """
 589        if self.iListDumpFile is None or not self.iListDumpFile:
 590            uLogger.error("Output name of dump file must be defined!")
 591            raise Exception("Filename required")
 592
 593        if not self.iList or forceUpdate:
 594            self.iList = self.Listing()
 595
 596        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 597
 598        # Save as XLSX with separated sheets for every type of instruments:
 599        with pd.ExcelWriter(
 600                path=xlsxDumpFile,
 601                date_format=TKS_DATE_FORMAT,
 602                datetime_format=TKS_DATE_TIME_FORMAT,
 603                mode="w",
 604        ) as writer:
 605            for iType in TKS_INSTRUMENTS:
 606                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 607                df = df[sorted(df)]  # sorted by column names
 608                df = df.applymap(
 609                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 610                    na_action="ignore",
 611                )  # converting numbers from nano-type to float in every cell
 612                df.to_excel(
 613                    writer,
 614                    sheet_name=iType,
 615                    encoding="UTF-8",
 616                    freeze_panes=(1, 1),
 617                )  # saving as XLSX-file with freeze first row and column as headers
 618
 619        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 620
 621    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 622        """
 623        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 624        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 625
 626        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 627
 628        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 629                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 630        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 631        """
 632        if self.iListDumpFile is None or not self.iListDumpFile:
 633            uLogger.error("Output name of dump file must be defined!")
 634            raise Exception("Filename required")
 635
 636        if not self.iList or forceUpdate:
 637            self.iList = self.Listing()
 638
 639        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 640        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 641            fH.write(jsonDump)
 642
 643        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 644
 645        return jsonDump
 646
 647    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 648        """
 649        Show information about one instrument defined by json data and prints it in Markdown format.
 650
 651        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 652
 653        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 654        :param show: if `True` then also printing information about instrument and its current price.
 655        :return: multilines text in Markdown format with information about one instrument.
 656        """
 657        splitLine = "|                                                             |                                                        |\n"
 658        infoText = ""
 659
 660        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 661            info = [
 662                "# Main information\n\n",
 663                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 664                "| Parameters                                                  | Values                                                 |\n",
 665                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 666                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 667                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 668            ]
 669
 670            if "sector" in iJSON.keys() and iJSON["sector"]:
 671                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 672
 673            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 674                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 675
 676            info.extend([
 677                splitLine,
 678                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 679                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 680            ])
 681
 682            if "isin" in iJSON.keys() and iJSON["isin"]:
 683                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 684
 685            if "classCode" in iJSON.keys():
 686                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 687
 688            info.extend([
 689                splitLine,
 690                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 691                splitLine,
 692                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 693                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 694                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 695            ])
 696
 697            if iJSON["figi"]:
 698                self._figi = iJSON["figi"]
 699                iJSON = iJSON | self.RequestTradingStatus()
 700
 701                info.extend([
 702                    splitLine,
 703                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 704                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 705                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 706                ])
 707
 708            info.append(splitLine)
 709
 710            if "type" in iJSON.keys() and iJSON["type"]:
 711                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 712
 713                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 714                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 715
 716            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 717                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 718
 719            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 720                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 721
 722            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 723                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 724
 725            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 726                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 727
 728            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 729                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 730
 731            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 732                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 733
 734            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 735                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 736
 737            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 738                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 739
 740            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 741                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 742
 743            if "currency" in iJSON.keys():
 744                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 745
 746            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 747                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 748
 749            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 750                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 751
 752            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 753                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 754
 755            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 756                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 759                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 762                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 765                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 768                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 769
 770            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 771                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 772
 773            iExt = None
 774            if iJSON["type"] == "Bonds":
 775                info.extend([
 776                    splitLine,
 777                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 778                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 779                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 780                        iJSON["nominal"]["currency"],
 781                    )),
 782                ])
 783
 784                if "floatingCouponFlag" in iJSON.keys():
 785                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 786
 787                if "amortizationFlag" in iJSON.keys():
 788                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 789
 790                info.append(splitLine)
 791
 792                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 793                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 794
 795                if iJSON["figi"]:
 796                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 797
 798                    info.extend([
 799                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 800                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 801                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 802                    ])
 803
 804                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 805                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 806                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 807                        iJSON["aciValue"]["currency"]
 808                    )))
 809
 810            if "currentPrice" in iJSON.keys():
 811                info.append(splitLine)
 812
 813                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 814                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 815
 816                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 817                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 818                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 819                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 820                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 821
 822                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 823                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 824
 825                info.extend([
 826                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 827                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 828                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 829                    )),
 830                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 831                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 832                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 833                    )),
 834                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 835                        "{:.2f}%{}".format(
 836                            iJSON["currentPrice"]["changes"],
 837                            " ({}{:.2f} {})".format(
 838                                "+" if bondChangesDelta > 0 else "",
 839                                bondChangesDelta,
 840                                aciCurrency
 841                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 842                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 843                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 844                                currency
 845                            ),
 846                        )
 847                    ),
 848                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 849                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 850                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 851                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 852                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 853                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 854                    )),
 855                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 856                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 859                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 860                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 861                    )),
 862                ])
 863
 864            if "lot" in iJSON.keys():
 865                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 866
 867            if "step" in iJSON.keys() and iJSON["step"] != 0:
 868                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 869
 870            # Add bond payment calendar:
 871            if iJSON["type"] == "Bonds":
 872                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 873                info.extend(["\n#", strCalendar])
 874
 875            infoText += "".join(info)
 876
 877            if show:
 878                uLogger.info("{}".format(infoText))
 879
 880            else:
 881                uLogger.debug("{}".format(infoText))
 882
 883            if self.infoFile is not None:
 884                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 885                    fH.write(infoText)
 886
 887                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 888
 889                if self.useHTMLReports:
 890                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 891                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 892                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 893
 894                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 895
 896        return infoText
 897
 898    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 899        """
 900        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 901
 902        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 903        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 904        :return: JSON formatted data with information about instrument.
 905        """
 906        tickerJSON = {}
 907        if self.moreDebug:
 908            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 909
 910        if not self._ticker:
 911            uLogger.warning("self._ticker variable is not be empty!")
 912
 913        else:
 914            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 915                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 916                raise Exception("Instrument not allowed")
 917
 918            if not self.iList:
 919                self.iList = self.Listing()
 920
 921            if self._ticker in self.iList["Shares"].keys():
 922                tickerJSON = self.iList["Shares"][self._ticker]
 923                if self.moreDebug:
 924                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 925
 926            elif self._ticker in self.iList["Currencies"].keys():
 927                tickerJSON = self.iList["Currencies"][self._ticker]
 928                if self.moreDebug:
 929                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 930
 931            elif self._ticker in self.iList["Bonds"].keys():
 932                tickerJSON = self.iList["Bonds"][self._ticker]
 933                if self.moreDebug:
 934                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 935
 936            elif self._ticker in self.iList["Etfs"].keys():
 937                tickerJSON = self.iList["Etfs"][self._ticker]
 938                if self.moreDebug:
 939                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 940
 941            elif self._ticker in self.iList["Futures"].keys():
 942                tickerJSON = self.iList["Futures"][self._ticker]
 943                if self.moreDebug:
 944                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 945
 946        if tickerJSON:
 947            self._figi = tickerJSON["figi"]
 948
 949            if requestPrice:
 950                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 951
 952                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 953                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 954
 955                else:
 956                    tickerJSON["currentPrice"]["changes"] = 0
 957
 958            if show:
 959                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 960
 961        else:
 962            if show:
 963                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 964
 965        return tickerJSON
 966
 967    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 968        """
 969        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 970
 971        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 972        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 973        :return: JSON formatted data with information about instrument.
 974        """
 975        figiJSON = {}
 976        if self.moreDebug:
 977            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 978
 979        if not self._figi:
 980            uLogger.warning("self._figi variable is not be empty!")
 981
 982        else:
 983            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 984                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 985                raise Exception("Instrument not allowed")
 986
 987            if not self.iList:
 988                self.iList = self.Listing()
 989
 990            for item in self.iList["Shares"].keys():
 991                if self._figi == self.iList["Shares"][item]["figi"]:
 992                    figiJSON = self.iList["Shares"][item]
 993
 994                    if self.moreDebug:
 995                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 996
 997                    break
 998
 999            if not figiJSON:
1000                for item in self.iList["Currencies"].keys():
1001                    if self._figi == self.iList["Currencies"][item]["figi"]:
1002                        figiJSON = self.iList["Currencies"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Bonds"].keys():
1011                    if self._figi == self.iList["Bonds"][item]["figi"]:
1012                        figiJSON = self.iList["Bonds"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1016
1017                        break
1018
1019            if not figiJSON:
1020                for item in self.iList["Etfs"].keys():
1021                    if self._figi == self.iList["Etfs"][item]["figi"]:
1022                        figiJSON = self.iList["Etfs"][item]
1023
1024                        if self.moreDebug:
1025                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1026
1027                        break
1028
1029            if not figiJSON:
1030                for item in self.iList["Futures"].keys():
1031                    if self._figi == self.iList["Futures"][item]["figi"]:
1032                        figiJSON = self.iList["Futures"][item]
1033
1034                        if self.moreDebug:
1035                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1036
1037                        break
1038
1039        if figiJSON:
1040            self._figi = figiJSON["figi"]
1041            self._ticker = figiJSON["ticker"]
1042
1043            if requestPrice:
1044                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1045
1046                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1047                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1048
1049                else:
1050                    figiJSON["currentPrice"]["changes"] = 0
1051
1052            if show:
1053                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1054
1055        else:
1056            if show:
1057                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1058
1059        return figiJSON
1060
1061    def GetCurrentPrices(self, show: bool = True) -> dict:
1062        """
1063        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1064        `{"buy": [{"price": 1243.8, "quantity": 193},
1065                  {"price": 1244.0, "quantity": 168},
1066                  {"price": 1244.8, "quantity": 5},
1067                  {"price": 1245.0, "quantity": 61},
1068                  {"price": 1245.4, "quantity": 60}],
1069          "sell": [{"price": 1243.6, "quantity": 8},
1070                   {"price": 1242.6, "quantity": 10},
1071                   {"price": 1242.4, "quantity": 18},
1072                   {"price": 1242.2, "quantity": 50},
1073                   {"price": 1242.0, "quantity": 113}],
1074          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1075        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1076        - sell: list of dicts with Buyers prices,
1077            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1078            - quantity: volume value by current price in lots,
1079        - limitUp: current trade session limit price, maximum,
1080        - limitDown: current trade session limit price, minimum,
1081        - lastPrice: last deal price of the instrument,
1082        - closePrice: previous trade session close price of the instrument.
1083
1084        See also: `SearchByTicker()` and `SearchByFIGI()`.
1085        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1086        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1087
1088        :param show: if `True` then print DOM to log and console.
1089        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1090                 If an error occurred then returns an empty record:
1091                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1092        """
1093        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1094
1095        if self.depth < 1:
1096            uLogger.error("Depth of Market (DOM) must be >=1!")
1097            raise Exception("Incorrect value")
1098
1099        if not (self._ticker or self._figi):
1100            uLogger.error("self._ticker or self._figi variables must be defined!")
1101            raise Exception("Ticker or FIGI required")
1102
1103        if self._ticker and not self._figi:
1104            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1106
1107        if not self._ticker and self._figi:
1108            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1110
1111        if not self._figi:
1112            uLogger.error("FIGI is not defined!")
1113            raise Exception("Ticker or FIGI required")
1114
1115        else:
1116            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1117
1118            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1120            self.body = str({"figi": self._figi, "depth": self.depth})
1121            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1124                # list of dicts with sellers orders:
1125                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1126
1127                # list of dicts with buyers orders:
1128                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1129
1130                # max price of instrument at this time:
1131                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1132
1133                # min price of instrument at this time:
1134                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1135
1136                # last price of deal with instrument:
1137                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1138
1139                # last close price of instrument:
1140                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1141
1142            else:
1143                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1144                uLogger.debug("Server response: {}".format(pricesResponse))
1145
1146            if show:
1147                if prices["buy"] or prices["sell"]:
1148                    info = [
1149                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1150                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1151                            self._ticker,
1152                            self._figi,
1153                            self.depth,
1154                        ),
1155                        "-" * 60, "\n",
1156                        "             Orders of Buyers | Orders of Sellers\n",
1157                        "-" * 60, "\n",
1158                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1159                        "-" * 60, "\n",
1160                    ]
1161
1162                    if not prices["buy"]:
1163                        info.append("                              | No orders!\n")
1164                        sumBuy = 0
1165
1166                    else:
1167                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1168                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1169                        for item in maxMinSorted:
1170                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1171
1172                    if not prices["sell"]:
1173                        info.append("No orders!                    |\n")
1174                        sumSell = 0
1175
1176                    else:
1177                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1178                        for item in prices["sell"]:
1179                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1180
1181                    info.extend([
1182                        "-" * 60, "\n",
1183                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1184                        "-" * 60, "\n",
1185                    ])
1186
1187                    infoText = "".join(info)
1188
1189                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1190
1191                else:
1192                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1193
1194        return prices
1195
1196    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1197        """
1198        This method get and show information about all available broker instruments for current user account.
1199        If `instrumentsFile` string is not empty then also save information to this file.
1200
1201        :param show: if `True` then print results to console, if `False` — print only to file.
1202        :return: multi-lines string with all available broker instruments
1203        """
1204        if not self.iList:
1205            self.iList = self.Listing()
1206
1207        info = [
1208            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1209            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1210        ]
1211
1212        # add instruments count by type:
1213        for iType in self.iList.keys():
1214            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1215
1216        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1217        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1218
1219        # generating info tables with all instruments by type:
1220        for iType in self.iList.keys():
1221            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1222
1223            for instrument in self.iList[iType].keys():
1224                iName = self.iList[iType][instrument]["name"]  # instrument's name
1225                if len(iName) > 57:
1226                    iName = "{}...".format(iName[:54])  # right trim for a long string
1227
1228                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1229                    self.iList[iType][instrument]["ticker"],
1230                    iName,
1231                    self.iList[iType][instrument]["figi"],
1232                    self.iList[iType][instrument]["currency"],
1233                    self.iList[iType][instrument]["lot"],
1234                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1235                ))
1236
1237        infoText = "".join(info)
1238
1239        if show:
1240            uLogger.info(infoText)
1241
1242        if self.instrumentsFile:
1243            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1244                fH.write(infoText)
1245
1246            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1247
1248            if self.useHTMLReports:
1249                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1250                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1251                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1252
1253                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1254
1255        return infoText
1256
1257    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1258        """
1259        This method search and show information about instruments by part of its ticker, FIGI or name.
1260        If `searchResultsFile` string is not empty then also save information to this file.
1261
1262        :param pattern: string with part of ticker, FIGI or instrument's name.
1263        :param show: if `True` then print results to console, if `False` — return list of result only.
1264        :return: list of dictionaries with all found instruments.
1265        """
1266        if not self.iList:
1267            self.iList = self.Listing()
1268
1269        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1270        compiledPattern = re.compile(pattern, re.IGNORECASE)
1271
1272        for iType in self.iList:
1273            for instrument in self.iList[iType].values():
1274                searchResult = compiledPattern.search(" ".join(
1275                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1276                ))
1277
1278                if searchResult:
1279                    searchResults[iType][instrument["ticker"]] = instrument
1280
1281        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1282        info = [
1283            "# Search results\n\n",
1284            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1285            "* **Search pattern:** [{}]\n".format(pattern),
1286            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1287            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1288        ]
1289        infoShort = info[:]
1290
1291        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1292        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1293        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1294
1295        if resultsLen == 0:
1296            info.append("\nNo results\n")
1297            infoShort.append("\nNo results\n")
1298            uLogger.warning("No results. Try changing your search pattern.")
1299
1300        else:
1301            for iType in searchResults:
1302                iTypeValuesCount = len(searchResults[iType].values())
1303                if iTypeValuesCount > 0:
1304                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1306
1307                    for instrument in searchResults[iType].values():
1308                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1309                            instrument["type"],
1310                            instrument["ticker"],
1311                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1312                            instrument["figi"],
1313                        ))
1314
1315                    if iTypeValuesCount <= 5:
1316                        infoShort.extend(info[-iTypeValuesCount:])
1317
1318                    else:
1319                        infoShort.extend(info[-5:])
1320                        infoShort.append(skippedLine)
1321
1322        infoText = "".join(info)
1323        infoTextShort = "".join(infoShort)
1324
1325        if show:
1326            uLogger.info(infoTextShort)
1327            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1328
1329        if self.searchResultsFile:
1330            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1331                fH.write(infoText)
1332
1333            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1334
1335            if self.useHTMLReports:
1336                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1337                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1338                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1339
1340                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1341
1342        return searchResults
1343
1344    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1345        """
1346        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1347
1348        :param instruments: list of strings with tickers or FIGIs.
1349        :return: list with unique instrument FIGIs only.
1350        """
1351        requestedInstruments = []
1352        for iName in instruments:
1353            if iName not in self.aliases.keys():
1354                if iName not in requestedInstruments:
1355                    requestedInstruments.append(iName)
1356
1357            else:
1358                if iName not in requestedInstruments:
1359                    if self.aliases[iName] not in requestedInstruments:
1360                        requestedInstruments.append(self.aliases[iName])
1361
1362        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1363
1364        onlyUniqueFIGIs = []
1365        for iName in requestedInstruments:
1366            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1367                continue
1368
1369            self._ticker = iName
1370            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1371
1372            if not iData:
1373                self._ticker = ""
1374                self._figi = iName
1375
1376                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1377
1378                if not iData:
1379                    self._figi = ""
1380                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1381
1382            if iData and iData["figi"] not in onlyUniqueFIGIs:
1383                onlyUniqueFIGIs.append(iData["figi"])
1384
1385        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1386
1387        return onlyUniqueFIGIs
1388
1389    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1390        """
1391        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1392
1393        See limits: https://tinkoff.github.io/investAPI/limits/
1394
1395        If `pricesFile` string is not empty then also save information to this file.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1399        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1400                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1401        """
1402        if instruments is None or not instruments:
1403            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1404            raise Exception("Ticker or FIGI required")
1405
1406        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1407
1408        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1409
1410        iList = []  # trying to get info and current prices about all unique instruments:
1411        for self._figi in onlyUniqueFIGIs:
1412            iData = self.SearchByFIGI(requestPrice=True)
1413            iList.append(iData)
1414
1415        self.ShowListOfPrices(iList, show)
1416
1417        return iList
1418
1419    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1420        """
1421        Show table contains current prices of given instruments.
1422
1423        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1424                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :return: multilines text in Markdown format as a table contains current prices.
1427        """
1428        infoText = ""
1429
1430        if show or self.pricesFile:
1431            info = [
1432                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1433                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1434                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1435            ]
1436
1437            for item in iList:
1438                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1439                    item["ticker"],
1440                    item["figi"],
1441                    item["type"],
1442                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1443                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1444                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1447                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1448                    ),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1451                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1452                    ),
1453                    item["currency"],
1454                ))
1455
1456            infoText = "".join(info)
1457
1458            if show:
1459                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1460
1461            if self.pricesFile:
1462                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1463                    fH.write(infoText)
1464
1465                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1466
1467                if self.useHTMLReports:
1468                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1469                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1470                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1471
1472                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1473
1474        return infoText
1475
1476    def RequestTradingStatus(self) -> dict:
1477        """
1478        Requesting trading status for the instrument defined by `figi` variable.
1479
1480        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1481
1482        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1483
1484        :return: dictionary with trading status attributes. Response example:
1485                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1486                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1487        """
1488        if self._figi is None or not self._figi:
1489            uLogger.error("Variable `figi` must be defined for using this method!")
1490            raise Exception("FIGI required")
1491
1492        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1493
1494        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1495        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1496        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1497
1498        if self.moreDebug:
1499            uLogger.debug("Records about current trading status successfully received")
1500
1501        return tradingStatus
1502
1503    def RequestPortfolio(self) -> dict:
1504        """
1505        Requesting actual user's portfolio for current `accountId`.
1506
1507        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1508
1509        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1510
1511        :return: dictionary with user's portfolio.
1512        """
1513        if self.accountId is None or not self.accountId:
1514            uLogger.error("Variable `accountId` must be defined for using this method!")
1515            raise Exception("Account ID required")
1516
1517        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1518
1519        self.body = str({"accountId": self.accountId})
1520        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1521        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1522
1523        if self.moreDebug:
1524            uLogger.debug("Records about user's portfolio successfully received")
1525
1526        return rawPortfolio
1527
1528    def RequestPositions(self) -> dict:
1529        """
1530        Requesting open positions by currencies and instruments for current `accountId`.
1531
1532        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1533
1534        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1535
1536        :return: dictionary with open positions by instruments.
1537        """
1538        if self.accountId is None or not self.accountId:
1539            uLogger.error("Variable `accountId` must be defined for using this method!")
1540            raise Exception("Account ID required")
1541
1542        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1543
1544        self.body = str({"accountId": self.accountId})
1545        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1546        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1547
1548        if self.moreDebug:
1549            uLogger.debug("Records about current open positions successfully received")
1550
1551        return rawPositions
1552
1553    def RequestPendingOrders(self) -> list:
1554        """
1555        Requesting current actual pending limit orders for current `accountId`.
1556
1557        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1558
1559        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1560
1561        :return: list of dictionaries with pending limit orders.
1562        """
1563        if self.accountId is None or not self.accountId:
1564            uLogger.error("Variable `accountId` must be defined for using this method!")
1565            raise Exception("Account ID required")
1566
1567        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1568
1569        self.body = str({"accountId": self.accountId})
1570        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1571        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1572
1573        if "orders" in rawResponse.keys():
1574            rawOrders = rawResponse["orders"]
1575            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1576
1577        else:
1578            rawOrders = []
1579            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1580
1581        return rawOrders
1582
1583    def RequestStopOrders(self) -> list:
1584        """
1585        Requesting current actual stop orders for current `accountId`.
1586
1587        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1588
1589        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1590
1591        :return: list of dictionaries with stop orders.
1592        """
1593        if self.accountId is None or not self.accountId:
1594            uLogger.error("Variable `accountId` must be defined for using this method!")
1595            raise Exception("Account ID required")
1596
1597        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1598
1599        self.body = str({"accountId": self.accountId})
1600        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1601        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1602
1603        if "stopOrders" in rawResponse.keys():
1604            rawStopOrders = rawResponse["stopOrders"]
1605            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1606
1607        else:
1608            rawStopOrders = []
1609            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1610
1611        return rawStopOrders
1612
1613    def Overview(self, show: bool = False, details: str = "full") -> dict:
1614        """
1615        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1616        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1617        and `overviewBondsCalendarFile` are defined then also save information to file.
1618
1619        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1620        many requests about the state of the portfolio, and then, based on the received data, a large number
1621        of calculation and statistics are collected.
1622
1623        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1624        :param details: how detailed should the information be?
1625        - `full` — shows full available information about portfolio status (by default),
1626        - `positions` — shows only open positions,
1627        - `orders` — shows only sections of open limits and stop orders.
1628        - `digest` — show a short digest of the portfolio status,
1629        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1630        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1631        :return: dictionary with client's raw portfolio and some statistics.
1632        """
1633        if self.accountId is None or not self.accountId:
1634            uLogger.error("Variable `accountId` must be defined for using this method!")
1635            raise Exception("Account ID required")
1636
1637        view = {
1638            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1639                "headers": {},  # list of dictionaries, response headers without "positions" section
1640                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1641                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1642                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1643                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1644                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1645                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1646                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1647                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1648                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1649            },
1650            "stat": {  # --- some statistics calculated using "raw" sections:
1651                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1652                "availableRUB": 0.,  # available rubles (without other currencies)
1653                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1654                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1655                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1656                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1657                "sharesCostRUB": 0.,  # costs of all shares in RUB
1658                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1659                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1660                "futuresCostRUB": 0.,  # costs of all futures in RUB
1661                "Currencies": [],  # list of dictionaries of all currencies statistics
1662                "Shares": [],  # list of dictionaries of all shares statistics
1663                "Bonds": [],  # list of dictionaries of all bonds statistics
1664                "Etfs": [],  # list of dictionaries of all etfs statistics
1665                "Futures": [],  # list of dictionaries of all futures statistics
1666                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1667                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1668                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1669                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1670                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1671            },
1672            "analytics": {  # --- some analytics of portfolio:
1673                "distrByAssets": {},  # portfolio distribution by assets
1674                "distrByCompanies": {},  # portfolio distribution by companies
1675                "distrBySectors": {},  # portfolio distribution by sectors
1676                "distrByCurrencies": {},  # portfolio distribution by currencies
1677                "distrByCountries": {},  # portfolio distribution by countries
1678                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1679            }
1680        }
1681
1682        details = details.lower()
1683        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1684        if details not in availableDetails:
1685            details = "full"
1686            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1687
1688        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1689
1690        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1691        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1692        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1693        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1694
1695        # save response headers without "positions" section:
1696        for key in portfolioResponse.keys():
1697            if key != "positions":
1698                view["raw"]["headers"][key] = portfolioResponse[key]
1699
1700            else:
1701                continue
1702
1703        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1704        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1705        for item in portfolioResponse["positions"]:
1706            if item["instrumentType"] == "currency":
1707                self._figi = item["figi"]
1708                curr = self.SearchByFIGI(requestPrice=False)
1709
1710                # current price of currency in RUB:
1711                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1712                    "name": curr["name"],
1713                    "currentPrice": NanoToFloat(
1714                        item["currentPrice"]["units"],
1715                        item["currentPrice"]["nano"]
1716                    ),
1717                }
1718
1719                view["raw"]["Currencies"].append(item)
1720
1721            elif item["instrumentType"] == "share":
1722                view["raw"]["Shares"].append(item)
1723
1724            elif item["instrumentType"] == "bond":
1725                view["raw"]["Bonds"].append(item)
1726
1727            elif item["instrumentType"] == "etf":
1728                view["raw"]["Etfs"].append(item)
1729
1730            elif item["instrumentType"] == "futures":
1731                view["raw"]["Futures"].append(item)
1732
1733            else:
1734                continue
1735
1736        # how many volume of currencies (by ISO currency name) are blocked:
1737        for item in view["raw"]["positions"]["blocked"]:
1738            blocked = NanoToFloat(item["units"], item["nano"])
1739            if blocked > 0:
1740                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1741
1742        # how many volume of instruments (by FIGI) are blocked:
1743        for item in view["raw"]["positions"]["securities"]:
1744            blocked = int(item["blocked"])
1745            if blocked > 0:
1746                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1747
1748        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1749
1750        if "rub" in allBlocked.keys():
1751            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1752
1753        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1754        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1755        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1756        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1757        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1758        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1759        view["stat"]["portfolioCostRUB"] = sum([
1760            view["stat"]["allCurrenciesCostRUB"],
1761            view["stat"]["sharesCostRUB"],
1762            view["stat"]["bondsCostRUB"],
1763            view["stat"]["etfsCostRUB"],
1764            view["stat"]["futuresCostRUB"],
1765        ])
1766
1767        # --- calculating some portfolio statistics:
1768        byComp = {}  # distribution by companies
1769        bySect = {}  # distribution by sectors
1770        byCurr = {}  # distribution by currencies (include RUB)
1771        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1772        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1773
1774        for item in portfolioResponse["positions"]:
1775            self._figi = item["figi"]
1776            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1777
1778            if instrument:
1779                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1780                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1781
1782                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1783                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1784
1785                else:
1786                    blocked = 0
1787
1788                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1789                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1790                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1791                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1792                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1793                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1794                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1795                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1796                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1797                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1798                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1799                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1800
1801                statData = {
1802                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1803                    "ticker": instrument["ticker"],  # ticker by FIGI
1804                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1805                    "volume": volume,  # available volume of instrument
1806                    "lots": lots,  # volume in lots of instrument
1807                    "direction": direction,  # direction of an instrument's position: short or long
1808                    "blocked": blocked,  # blocked volume of currency or instrument
1809                    "currentPrice": curPrice,  # current instrument's price in basic asset
1810                    "average": average,  # current average position price
1811                    "cost": cost,  # current cost of all volume of instrument in basic asset
1812                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1813                    "costRUB": costRUB,  # cost of instrument in ruble
1814                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1815                    "profit": profit,  # expected profit at current moment
1816                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1817                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1818                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1819                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1820                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1821                    "step": instrument["step"],  # minimum price increment
1822                }
1823
1824                # adding distribution by unique countries:
1825                if statData["country"] not in byCountry.keys():
1826                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1827
1828                else:
1829                    byCountry[statData["country"]]["cost"] += costRUB
1830                    byCountry[statData["country"]]["percent"] += percentCostRUB
1831
1832                if item["instrumentType"] != "currency":
1833                    # adding distribution by unique companies:
1834                    if statData["name"]:
1835                        if statData["name"] not in byComp.keys():
1836                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1837
1838                        else:
1839                            byComp[statData["name"]]["cost"] += costRUB
1840                            byComp[statData["name"]]["percent"] += percentCostRUB
1841
1842                    # adding distribution by unique sectors:
1843                    if statData["sector"] not in bySect.keys():
1844                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1845
1846                    else:
1847                        bySect[statData["sector"]]["cost"] += costRUB
1848                        bySect[statData["sector"]]["percent"] += percentCostRUB
1849
1850                # adding distribution by unique currencies:
1851                if currency not in byCurr.keys():
1852                    byCurr[currency] = {
1853                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1854                        "cost": costRUB,
1855                        "percent": percentCostRUB
1856                    }
1857
1858                else:
1859                    byCurr[currency]["cost"] += costRUB
1860                    byCurr[currency]["percent"] += percentCostRUB
1861
1862                # saving statistics for every instrument:
1863                if item["instrumentType"] == "currency":
1864                    view["stat"]["Currencies"].append(statData)
1865
1866                    # update dict with free funds for trading (total - blocked) by currencies
1867                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1868                    view["stat"]["funds"][currency] = {
1869                        "total": volume,
1870                        "totalCostRUB": costRUB,  # total volume cost in rubles
1871                        "free": volume - blocked,
1872                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1873                    }
1874
1875                elif item["instrumentType"] == "share":
1876                    view["stat"]["Shares"].append(statData)
1877
1878                elif item["instrumentType"] == "bond":
1879                    view["stat"]["Bonds"].append(statData)
1880
1881                elif item["instrumentType"] == "etf":
1882                    view["stat"]["Etfs"].append(statData)
1883
1884                elif item["instrumentType"] == "Futures":
1885                    view["stat"]["Futures"].append(statData)
1886
1887                else:
1888                    continue
1889
1890        # total changes in Russian Ruble:
1891        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1892        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1893        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1894        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1895        view["stat"]["funds"]["rub"] = {
1896            "total": view["stat"]["availableRUB"],
1897            "totalCostRUB": view["stat"]["availableRUB"],
1898            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1899            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1900        }
1901
1902        # --- pending limit orders sector data:
1903        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1904        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1905
1906        for item in view["raw"]["orders"]:
1907            self._figi = item["figi"]
1908
1909            if item["figi"] not in uniquePendingOrdersFIGIs:
1910                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1911
1912                uniquePendingOrdersFIGIs.append(item["figi"])
1913                uniquePendingOrders[item["figi"]] = instrument
1914
1915            else:
1916                instrument = uniquePendingOrders[item["figi"]]
1917
1918            if instrument:
1919                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1920                orderType = TKS_ORDER_TYPES[item["orderType"]]
1921                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1922                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1923
1924                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1925                if item["direction"] == "ORDER_DIRECTION_BUY":
1926                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1927
1928                else:
1929                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1930
1931                # requested price for order execution:
1932                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1933
1934                # necessary changes in percent to reach target from current price:
1935                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1936
1937                view["stat"]["orders"].append({
1938                    "orderID": item["orderId"],  # orderId number parameter of current order
1939                    "figi": item["figi"],  # FIGI identification
1940                    "ticker": instrument["ticker"],  # ticker name by FIGI
1941                    "lotsRequested": item["lotsRequested"],  # requested lots value
1942                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1943                    "currentPrice": lastPrice,  # current instrument's price for defined action
1944                    "targetPrice": target,  # requested price for order execution in base currency
1945                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1946                    "percentChanges": changes,  # changes in percent to target from current price
1947                    "currency": item["currency"],  # instrument's currency name
1948                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1949                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1950                    "status": orderState,  # order status from TKS_ORDER_STATES
1951                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1952                })
1953
1954        # --- stop orders sector data:
1955        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1956        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1957
1958        for item in view["raw"]["stopOrders"]:
1959            self._figi = item["figi"]
1960
1961            if item["figi"] not in uniqueStopOrdersFIGIs:
1962                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1963
1964                uniqueStopOrdersFIGIs.append(item["figi"])
1965                uniqueStopOrders[item["figi"]] = instrument
1966
1967            else:
1968                instrument = uniqueStopOrders[item["figi"]]
1969
1970            if instrument:
1971                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1972                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1973                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1974
1975                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1976                if "expirationTime" in item.keys():
1977                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1978                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1979
1980                else:
1981                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1982                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1983
1984                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1985                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1986                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1987
1988                else:
1989                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1990
1991                # requested price when stop-order executed:
1992                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1993
1994                # price for limit-order, set up when stop-order executed:
1995                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1996
1997                # necessary changes in percent to reach target from current price:
1998                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1999
2000                view["stat"]["stopOrders"].append({
2001                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2002                    "figi": item["figi"],  # FIGI identification
2003                    "ticker": instrument["ticker"],  # ticker name by FIGI
2004                    "lotsRequested": item["lotsRequested"],  # requested lots value
2005                    "currentPrice": lastPrice,  # current instrument's price for defined action
2006                    "targetPrice": target,  # requested price for stop-order execution in base currency
2007                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2008                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2009                    "percentChanges": changes,  # changes in percent to target from current price
2010                    "currency": item["currency"],  # instrument's currency name
2011                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2012                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2013                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2014                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2015                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2016                })
2017
2018        # --- calculating data for analytics section:
2019        # portfolio distribution by assets:
2020        view["analytics"]["distrByAssets"] = {
2021            "Ruble": {
2022                "uniques": 1,
2023                "cost": view["stat"]["availableRUB"],
2024                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2025            },
2026            "Currencies": {
2027                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2028                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2029                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2030            },
2031            "Shares": {
2032                "uniques": len(view["stat"]["Shares"]),
2033                "cost": view["stat"]["sharesCostRUB"],
2034                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2035            },
2036            "Bonds": {
2037                "uniques": len(view["stat"]["Bonds"]),
2038                "cost": view["stat"]["bondsCostRUB"],
2039                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2040            },
2041            "Etfs": {
2042                "uniques": len(view["stat"]["Etfs"]),
2043                "cost": view["stat"]["etfsCostRUB"],
2044                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2045            },
2046            "Futures": {
2047                "uniques": len(view["stat"]["Futures"]),
2048                "cost": view["stat"]["futuresCostRUB"],
2049                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2050            },
2051        }
2052
2053        # portfolio distribution by companies:
2054        view["analytics"]["distrByCompanies"]["All money cash"] = {
2055            "ticker": "",
2056            "cost": view["stat"]["allCurrenciesCostRUB"],
2057            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058        }
2059        view["analytics"]["distrByCompanies"].update(byComp)
2060
2061        # portfolio distribution by sectors:
2062        view["analytics"]["distrBySectors"]["All money cash"] = {
2063            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2064            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2065        }
2066        view["analytics"]["distrBySectors"].update(bySect)
2067
2068        # portfolio distribution by currencies:
2069        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2070            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2071
2072            if self.moreDebug:
2073                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2074
2075        view["analytics"]["distrByCurrencies"].update(byCurr)
2076        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2077        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2078
2079        # portfolio distribution by countries:
2080        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2081            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2082
2083            if self.moreDebug:
2084                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2085
2086        view["analytics"]["distrByCountries"].update(byCountry)
2087        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2088        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2089
2090        # --- Prepare text statistics overview in human-readable:
2091        if show:
2092            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2093
2094            # Whatever the value `details`, header not changes:
2095            info = [
2096                "# Client's portfolio\n\n",
2097                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2098                "* **Account ID:** [{}]\n".format(self.accountId),
2099            ]
2100
2101            if details in ["full", "positions", "digest"]:
2102                info.extend([
2103                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2104                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2105                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2106                        view["stat"]["totalChangesRUB"],
2107                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2108                        view["stat"]["totalChangesPercentRUB"],
2109                    ),
2110                ])
2111
2112            if details in ["full", "positions"]:
2113                info.extend([
2114                    "## Open positions\n\n",
2115                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2116                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2117                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2118                        "{:.2f} ({:.2f}) rub".format(
2119                            view["stat"]["availableRUB"],
2120                            view["stat"]["blockedRUB"],
2121                        )
2122                    )
2123                ])
2124
2125                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2126                    return [
2127                        "|                             |                                 |          |              |              |                     |                              |\n",
2128                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2129                            noTradeStr if noTradeStr else typeStr,
2130                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2131                        ),
2132                    ]
2133
2134                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2135                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2136                        "{} [{}]".format(data["ticker"], data["figi"]),
2137                        "{:.2f} ({:.2f}) {}".format(
2138                            data["volume"],
2139                            data["blocked"],
2140                            data["currency"],
2141                        ) if isCurr else "{:.0f} ({:.0f})".format(
2142                            data["volume"],
2143                            data["blocked"],
2144                        ),
2145                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2146                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2147                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2148                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2149                        "{}{:.2f} {} ({}{:.2f}%)".format(
2150                            "+" if data["profit"] > 0 else "",
2151                            data["profit"], data["baseCurrencyName"],
2152                            "+" if data["percentProfit"] > 0 else "",
2153                            data["percentProfit"],
2154                        ),
2155                    )
2156
2157                # --- Show currencies section:
2158                if view["stat"]["Currencies"]:
2159                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2160                    for item in view["stat"]["Currencies"]:
2161                        info.append(_InfoStr(item, isCurr=True))
2162
2163                else:
2164                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2165
2166                # --- Show shares section:
2167                if view["stat"]["Shares"]:
2168                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2169
2170                    for item in view["stat"]["Shares"]:
2171                        info.append(_InfoStr(item))
2172
2173                else:
2174                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2175
2176                # --- Show bonds section:
2177                if view["stat"]["Bonds"]:
2178                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2179
2180                    for item in view["stat"]["Bonds"]:
2181                        info.append(_InfoStr(item))
2182
2183                else:
2184                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2185
2186                # --- Show etfs section:
2187                if view["stat"]["Etfs"]:
2188                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2189
2190                    for item in view["stat"]["Etfs"]:
2191                        info.append(_InfoStr(item))
2192
2193                else:
2194                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2195
2196                # --- Show futures section:
2197                if view["stat"]["Futures"]:
2198                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2199
2200                    for item in view["stat"]["Futures"]:
2201                        info.append(_InfoStr(item))
2202
2203                else:
2204                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2205
2206            if details in ["full", "orders"]:
2207                # --- Show pending limit orders section:
2208                if view["stat"]["orders"]:
2209                    info.extend([
2210                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2211                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2212                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2213                    ])
2214
2215                    for item in view["stat"]["orders"]:
2216                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2217                            "{} [{}]".format(item["ticker"], item["figi"]),
2218                            item["orderID"],
2219                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2220                            "{} {} ({}{:.2f}%)".format(
2221                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2222                                item["baseCurrencyName"],
2223                                "+" if item["percentChanges"] > 0 else "",
2224                                float(item["percentChanges"]),
2225                            ),
2226                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2227                            item["action"],
2228                            item["type"],
2229                            item["date"],
2230                        ))
2231
2232                else:
2233                    info.append("\n## Total pending limit-orders: [0]\n")
2234
2235                # --- Show stop orders section:
2236                if view["stat"]["stopOrders"]:
2237                    info.extend([
2238                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2239                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2240                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2241                    ])
2242
2243                    for item in view["stat"]["stopOrders"]:
2244                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2245                            "{} [{}]".format(item["ticker"], item["figi"]),
2246                            item["orderID"],
2247                            item["lotsRequested"],
2248                            "{} {} ({}{:.2f}%)".format(
2249                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2250                                item["baseCurrencyName"],
2251                                "+" if item["percentChanges"] > 0 else "",
2252                                float(item["percentChanges"]),
2253                            ),
2254                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2255                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2256                            item["action"],
2257                            item["type"],
2258                            item["expType"],
2259                            item["createDate"],
2260                            item["expDate"],
2261                        ))
2262
2263                else:
2264                    info.append("\n## Total stop-orders: [0]\n")
2265
2266            if details in ["full", "analytics"]:
2267                # -- Show analytics section:
2268                if view["stat"]["portfolioCostRUB"] > 0:
2269                    info.extend([
2270                        "\n# Analytics\n\n"
2271                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2272                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2273                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2274                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2275                            view["stat"]["totalChangesRUB"],
2276                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2277                            view["stat"]["totalChangesPercentRUB"],
2278                        ),
2279                        "\n## Portfolio distribution by assets\n"
2280                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2281                        "|------------------------------------|---------|---------|--------------------|\n",
2282                    ])
2283
2284                    for key in view["analytics"]["distrByAssets"].keys():
2285                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2286                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2287                                key,
2288                                view["analytics"]["distrByAssets"][key]["uniques"],
2289                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2290                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2291                            ))
2292
2293                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2294
2295                    info.extend([
2296                        "\n## Portfolio distribution by companies\n"
2297                        "\n| Company                                      | Percent | Current cost       |\n",
2298                        aSepLine,
2299                    ])
2300
2301                    for company in view["analytics"]["distrByCompanies"].keys():
2302                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2303                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2304                                "{}{}".format(
2305                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2306                                    company,
2307                                ),
2308                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2309                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2310                            ))
2311
2312                    info.extend([
2313                        "\n## Portfolio distribution by sectors\n"
2314                        "\n| Sector                                       | Percent | Current cost       |\n",
2315                        aSepLine,
2316                    ])
2317
2318                    for sector in view["analytics"]["distrBySectors"].keys():
2319                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2320                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2321                                sector,
2322                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2323                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2324                            ))
2325
2326                    info.extend([
2327                        "\n## Portfolio distribution by currencies\n"
2328                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2329                        aSepLine,
2330                    ])
2331
2332                    for curr in view["analytics"]["distrByCurrencies"].keys():
2333                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2334                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2335                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2336                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2337                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2338                            ))
2339
2340                    info.extend([
2341                        "\n## Portfolio distribution by countries\n"
2342                        "\n| Assets by country                            | Percent | Current cost       |\n",
2343                        aSepLine,
2344                    ])
2345
2346                    for country in view["analytics"]["distrByCountries"].keys():
2347                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2348                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2349                                country,
2350                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2351                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2352                            ))
2353
2354            if details in ["full", "calendar"]:
2355                # -- Show bonds payment calendar section:
2356                if view["stat"]["Bonds"]:
2357                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2358                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2359                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2360
2361                else:
2362                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2363
2364            infoText = "".join(info)
2365
2366            uLogger.info(infoText)
2367
2368            if details == "full" and self.overviewFile:
2369                filename = self.overviewFile
2370
2371            elif details == "digest" and self.overviewDigestFile:
2372                filename = self.overviewDigestFile
2373
2374            elif details == "positions" and self.overviewPositionsFile:
2375                filename = self.overviewPositionsFile
2376
2377            elif details == "orders" and self.overviewOrdersFile:
2378                filename = self.overviewOrdersFile
2379
2380            elif details == "analytics" and self.overviewAnalyticsFile:
2381                filename = self.overviewAnalyticsFile
2382
2383            elif details == "calendar" and self.overviewBondsCalendarFile:
2384                filename = self.overviewBondsCalendarFile
2385
2386            else:
2387                filename = ""
2388
2389            if filename:
2390                with open(filename, "w", encoding="UTF-8") as fH:
2391                    fH.write(infoText)
2392
2393                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2394
2395                if self.useHTMLReports:
2396                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2397                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2398                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2399
2400                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2401
2402        return view
2403
2404    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2405        """
2406        Returns history operations between two given dates for current `accountId`.
2407        If `reportFile` string is not empty then also save human-readable report.
2408        Shows some statistical data of closed positions.
2409
2410        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2411        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2412        :param show: if `True` then also prints all records to the console.
2413        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2414        :return: original list of dictionaries with history of deals records from API ("operations" key):
2415                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2416                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2417        """
2418        if self.accountId is None or not self.accountId:
2419            uLogger.error("Variable `accountId` must be defined for using this method!")
2420            raise Exception("Account ID required")
2421
2422        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2423
2424        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2425
2426        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2427        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2428        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2429        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2430        customStat = {}  # custom statistics in additional to responseJSON
2431
2432        # --- output report in human-readable format:
2433        if show or self.reportFile:
2434            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2435            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2436            nextDay = ""
2437
2438            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2439
2440            if len(ops) > 0:
2441                customStat = {
2442                    "opsCount": 0,  # total operations count
2443                    "buyCount": 0,  # buy operations
2444                    "sellCount": 0,  # sell operations
2445                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2446                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2447                    "payIn": {"rub": 0.},  # Deposit brokerage account
2448                    "payOut": {"rub": 0.},  # Withdrawals
2449                    "divs": {"rub": 0.},  # Dividends income
2450                    "coupons": {"rub": 0.},  # Coupon's income
2451                    "brokerCom": {"rub": 0.},  # Service commissions
2452                    "serviceCom": {"rub": 0.},  # Service commissions
2453                    "marginCom": {"rub": 0.},  # Margin commissions
2454                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2455                }
2456
2457                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2458                for item in ops:
2459                    if item["state"] == "OPERATION_STATE_EXECUTED":
2460                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2461
2462                        # count buy operations:
2463                        if "_BUY" in item["operationType"]:
2464                            customStat["buyCount"] += 1
2465
2466                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2467                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2468
2469                            else:
2470                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2471
2472                        # count sell operations:
2473                        elif "_SELL" in item["operationType"]:
2474                            customStat["sellCount"] += 1
2475
2476                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2477                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2481
2482                        # count incoming operations:
2483                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2484                            if item["payment"]["currency"] in customStat["payIn"].keys():
2485                                customStat["payIn"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["payIn"][item["payment"]["currency"]] = payment
2489
2490                        # count withdrawals operations:
2491                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2492                            if item["payment"]["currency"] in customStat["payOut"].keys():
2493                                customStat["payOut"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["payOut"][item["payment"]["currency"]] = payment
2497
2498                        # count dividends income:
2499                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2500                            if item["payment"]["currency"] in customStat["divs"].keys():
2501                                customStat["divs"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["divs"][item["payment"]["currency"]] = payment
2505
2506                        # count coupon's income:
2507                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2508                            if item["payment"]["currency"] in customStat["coupons"].keys():
2509                                customStat["coupons"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["coupons"][item["payment"]["currency"]] = payment
2513
2514                        # count broker commissions:
2515                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2516                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2517                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2521
2522                        # count service commissions:
2523                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2524                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2525                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2529
2530                        # count margin commissions:
2531                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2532                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2533                                customStat["marginCom"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["marginCom"][item["payment"]["currency"]] = payment
2537
2538                        # count withholding taxes:
2539                        elif "_TAX" in item["operationType"]:
2540                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2541                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2545
2546                        else:
2547                            continue
2548
2549                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2550
2551                # --- view "Actions" lines:
2552                info.extend([
2553                    "| Report sections            |                               |                              |                      |                        |\n",
2554                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2555                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2556                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2557                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2558                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2559                    ),
2560                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2561                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2562                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2563                    ),
2564                ])
2565
2566                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2567                for key in opsKeys:
2568                    if key == "rub":
2569                        continue
2570
2571                    info.extend([
2572                        "|                            |                               | {:<28} |                      |                        |\n".format(
2573                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2574                        ),
2575                        "|                            |                               | {:<28} |                      |                        |\n".format(
2576                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2577                        ),
2578                    ])
2579
2580                info.append(splitLine1)
2581
2582                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2583                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2584                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2585                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2586                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2587                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2588                    )
2589
2590                # --- view "Payments" lines:
2591                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2592                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2593
2594                for key in paymentsKeys:
2595                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2596
2597                info.append(splitLine1)
2598
2599                # --- view "Commissions and taxes" lines:
2600                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2601                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2602
2603                for key in comKeys:
2604                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2605
2606                info.extend([
2607                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2608                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2609                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2610                ])
2611
2612            else:
2613                info.append("Broker returned no operations during this period\n")
2614
2615            # --- view "Operations" section:
2616            for item in ops:
2617                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2618                    continue
2619
2620                else:
2621                    self._figi = item["figi"] if item["figi"] else ""
2622                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2623                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2624
2625                    # group of deals during one day:
2626                    if nextDay and item["date"].split("T")[0] != nextDay:
2627                        info.append(splitLine2)
2628                        nextDay = ""
2629
2630                    else:
2631                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2632
2633                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2634                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2635                        self._figi if self._figi else "—",
2636                        instrument["ticker"] if instrument else "—",
2637                        instrument["type"] if instrument else "—",
2638                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2639                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2640                        TKS_OPERATION_STATES[item["state"]],
2641                        TKS_OPERATION_TYPES[item["operationType"]],
2642                    ))
2643
2644            infoText = "".join(info)
2645
2646            if show:
2647                if self.moreDebug:
2648                    uLogger.debug("Records about history of a client's operations successfully received")
2649
2650                uLogger.info(infoText)
2651
2652            if self.reportFile:
2653                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2654                    fH.write(infoText)
2655
2656                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2657
2658                if self.useHTMLReports:
2659                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2660                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2661                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2662
2663                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2664
2665        return ops, customStat
2666
2667    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2668        """
2669        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2670
2671        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2672        Warning! Broker server used ISO UTC time by default.
2673
2674        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2675        Also, `historyFile` used to update history with `onlyMissing` parameter.
2676
2677        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2678
2679        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2680        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2681        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2682                         `"hour"`, `"day"`. Default: `"hour"`.
2683        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2684                            False by default. Warning! History appends only from last candle to current time
2685                            with always update last candle!
2686        :param csvSep: separator if csv-file is used, `,` by default.
2687        :param show: if `True` then also prints Pandas DataFrame to the console.
2688        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2689                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2690        """
2691        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2692        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2693        history = None  # empty pandas object for history
2694
2695        if interval not in TKS_CANDLE_INTERVALS.keys():
2696            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2697            raise Exception("Incorrect value")
2698
2699        if not (self._ticker or self._figi):
2700            uLogger.error("Ticker or FIGI must be defined!")
2701            raise Exception("Ticker or FIGI required")
2702
2703        if self._ticker and not self._figi:
2704            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2705            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2706
2707        if self._figi and not self._ticker:
2708            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2709            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2710
2711        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2712        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2713        if interval.lower() != "day":
2714            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2715
2716        delta = dtEnd - dtStart  # current UTC time minus last time in file
2717        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2718
2719        # calculate history length in candles:
2720        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2721        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2722            length += 1  # to avoid fraction time
2723
2724        # calculate data blocks count:
2725        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2726
2727        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2728        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2729        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2730        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2731        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2732
2733        tempOld = None  # pandas object for old history, if --only-missing key present
2734        lastTime = None  # datetime object of last old candle in file
2735
2736        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2737            uLogger.debug("--only-missing key present, add only last missing candles...")
2738            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2739
2740            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2741
2742            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2743            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2744            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2745            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2746
2747            # get last datetime object from last string in file or minus 1 delta if file is empty:
2748            if len(tempOld) > 0:
2749                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2750
2751            else:
2752                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2753
2754            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2755
2756        responseJSONs = []  # raw history blocks of data
2757
2758        blockEnd = dtEnd
2759        for item in range(blocks):
2760            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2761            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2762
2763            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2764                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2765            ))
2766
2767            if blockStart == blockEnd:
2768                uLogger.debug("Skipped this zero-length block...")
2769
2770            else:
2771                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2772                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2773                self.body = str({
2774                    "figi": self._figi,
2775                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2776                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2777                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2778                })
2779                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2780
2781                if "code" in responseJSON.keys():
2782                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2783
2784                else:
2785                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2786                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2787
2788                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2789
2790            blockEnd = blockStart
2791
2792        printCount = len(responseJSONs)  # candles to show in console
2793        if responseJSONs:
2794            tempHistory = pd.DataFrame(
2795                data={
2796                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2797                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2798                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2799                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2800                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2801                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2802                    "volume": [int(item["volume"]) for item in responseJSONs],
2803                },
2804                index=range(len(responseJSONs)),
2805                columns=["date", "time", "open", "high", "low", "close", "volume"],
2806            )
2807            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2808            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2809
2810            # append only newest candles to old history if --only-missing key present:
2811            if onlyMissing and tempOld is not None and lastTime is not None:
2812                index = 0  # find start index in tempHistory data:
2813
2814                for i, item in tempHistory.iterrows():
2815                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2816
2817                    if curTime == lastTime:
2818                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2819                        index = i
2820                        printCount = index + 1
2821                        break
2822
2823                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2824
2825            else:
2826                history = tempHistory  # if no `--only-missing` key then load full data from server
2827
2828            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2829
2830        if history is not None and not history.empty:
2831            if show:
2832                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2833                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2834                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2835                ))
2836
2837        else:
2838            uLogger.warning("Received an empty candles history!")
2839
2840        if self.historyFile is not None:
2841            if history is not None and not history.empty:
2842                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2843                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2844
2845            else:
2846                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2847
2848        else:
2849            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2850
2851        return history
2852
2853    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2854        """
2855        Load candles history from csv-file and return Pandas DataFrame object.
2856
2857        See also: `History()` and `ShowHistoryChart()` methods.
2858
2859        :param filePath: path to csv-file to open.
2860        """
2861        loadedHistory = None  # init candles data object
2862
2863        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2864
2865        if os.path.exists(filePath):
2866            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2867
2868            tfStr = self.priceModel.FormattedDelta(
2869                self.priceModel.timeframe,
2870                "{days} days {hours}h {minutes}m {seconds}s",
2871            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2872                self.priceModel.timeframe,
2873                "{hours}h {minutes}m {seconds}s",
2874            )
2875
2876            if loadedHistory is not None and not loadedHistory.empty:
2877                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2878                    len(loadedHistory),
2879                    tfStr,
2880                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2881                )
2882
2883            else:
2884                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2885
2886        else:
2887            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2888
2889        return loadedHistory
2890
2891    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2892        """
2893        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2894
2895        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2896        Default: `index.html` (both for interact and non-interact candlesticks chart).
2897
2898        See also: `History()` and `LoadHistory()` methods.
2899
2900        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2901        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2902                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2903                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2904                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2905        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2906                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2907        """
2908        if isinstance(candles, str):
2909            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2910            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2911
2912        elif isinstance(candles, pd.DataFrame):
2913            self.priceModel.prices = candles  # set candles chain from variable
2914            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2915
2916            if "datetime" not in candles.columns:
2917                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2918
2919        else:
2920            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2921            raise Exception("Incorrect value")
2922
2923        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2924
2925        if interact:
2926            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2927
2928            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2929
2930        else:
2931            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2932
2933            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2934
2935        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2936
2937    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2938        """
2939        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2940        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2941
2942        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2943
2944        :param operation: string "Buy" or "Sell".
2945        :param lots: volume, integer count of lots >= 1.
2946        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2947        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2948        :param expDate: string "Undefined" by default or local date in future,
2949                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2950        :return: JSON with response from broker server.
2951        """
2952        if self.accountId is None or not self.accountId:
2953            uLogger.error("Variable `accountId` must be defined for using this method!")
2954            raise Exception("Account ID required")
2955
2956        if operation is None or not operation or operation not in ("Buy", "Sell"):
2957            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2958            raise Exception("Incorrect value")
2959
2960        if lots is None or lots < 1:
2961            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2962            lots = 1
2963
2964        if tp is None or tp < 0:
2965            tp = 0
2966
2967        if sl is None or sl < 0:
2968            sl = 0
2969
2970        if expDate is None or not expDate:
2971            expDate = "Undefined"
2972
2973        if not (self._ticker or self._figi):
2974            uLogger.error("Ticker or FIGI must be defined!")
2975            raise Exception("Ticker or FIGI required")
2976
2977        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2978        self._ticker = instrument["ticker"]
2979        self._figi = instrument["figi"]
2980
2981        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2982
2983        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2984        self.body = str({
2985            "figi": self._figi,
2986            "quantity": str(lots),
2987            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2988            "accountId": str(self.accountId),
2989            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2990        })
2991        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2992
2993        if "orderId" in response.keys():
2994            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2995                operation, response["orderId"],
2996                self._ticker, self._figi, lots,
2997                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2998                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2999                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3000            ))
3001
3002            if tp > 0:
3003                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3004
3005            if sl > 0:
3006                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3007
3008        else:
3009            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3010
3011        return response
3012
3013    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3014        """
3015        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3016        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3017
3018        See also: `Order()` and `Trade()` docstrings.
3019
3020        :param lots: volume, integer count of lots >= 1.
3021        :param tp: float > 0, take profit price of stop-order.
3022        :param sl: float > 0, stop loss price of stop-order.
3023        :param expDate: it's a local date in future.
3024                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3025        :return: JSON with response from broker server.
3026        """
3027        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3028
3029    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3030        """
3031        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3032        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3033
3034        See also: `Order()` and `Trade()` docstrings.
3035
3036        :param lots: volume, integer count of lots >= 1.
3037        :param tp: float > 0, take profit price of stop-order.
3038        :param sl: float > 0, stop loss price of stop-order.
3039        :param expDate: it's a local date in the future.
3040                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3041        :return: JSON with response from broker server.
3042        """
3043        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3044
3045    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3046        """
3047        Close position of given instruments.
3048
3049        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3050        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3051                         This avoids unnecessary downloading data from the server.
3052        """
3053        if instruments is None or not instruments:
3054            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3055            raise Exception("Ticker or FIGI required")
3056
3057        if isinstance(instruments, str):
3058            instruments = [instruments]
3059
3060        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3061        if uniqueInstruments:
3062            if portfolio is None or not portfolio:
3063                portfolio = self.Overview(show=False)
3064
3065            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3066            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3067
3068            for self._figi in uniqueInstruments:
3069                if self._figi not in allOpened:
3070                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3071                    continue
3072
3073                # search open trade info about instrument by ticker:
3074                instrument = {}
3075                for iType in TKS_INSTRUMENTS:
3076                    if instrument:
3077                        break
3078
3079                    for item in portfolio["stat"][iType]:
3080                        if item["figi"] == self._figi:
3081                            instrument = item
3082                            break
3083
3084                if instrument:
3085                    self._ticker = instrument["ticker"]
3086                    self._figi = instrument["figi"]
3087
3088                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3089                        self._ticker,
3090                        self._figi,
3091                        int(instrument["volume"]),
3092                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3093                    ))
3094
3095                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3096
3097                    if tradeLots > 0:
3098                        if instrument["blocked"] > 0:
3099                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3100                                instrument["blocked"],
3101                                self._ticker,
3102                                tradeLots,
3103                            ))
3104
3105                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3106                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3107
3108                    else:
3109                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3110
3111    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3112        """
3113        Close all positions of given instruments with defined type.
3114
3115        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3116        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3117                         This avoids unnecessary downloading data from the server.
3118        """
3119        if iType not in TKS_INSTRUMENTS:
3120            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3121
3122        else:
3123            if portfolio is None or not portfolio:
3124                portfolio = self.Overview(show=False)
3125
3126            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3127            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3128
3129            if tickers and portfolio:
3130                self.CloseTrades(tickers, portfolio)
3131
3132            else:
3133                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3134
3135    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3136        """
3137        Universal method to create market or limit orders with all available parameters for current `accountId`.
3138        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3139
3140        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3141        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3142
3143        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3144        then broker immediately open market order as you can do simple --buy or --sell operations!
3145
3146        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3147        When current price will go up or down to target price value then broker opens a limit order.
3148        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3149
3150        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3151
3152        :param operation: string "Buy" or "Sell".
3153        :param orderType: string "Limit" or "Stop".
3154        :param lots: volume, integer count of lots >= 1.
3155        :param targetPrice: target price > 0. This is open trade price for limit order.
3156        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3157                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3158        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3159                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3160                         Stop loss order always executed by market price.
3161        :param expDate: string "Undefined" by default or local date in future.
3162                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3163                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3164                        A limit order has no expiration date, it lasts until the end of the trading day.
3165        :return: JSON with response from broker server.
3166        """
3167        if self.accountId is None or not self.accountId:
3168            uLogger.error("Variable `accountId` must be defined for using this method!")
3169            raise Exception("Account ID required")
3170
3171        if operation is None or not operation or operation not in ("Buy", "Sell"):
3172            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3173            raise Exception("Incorrect value")
3174
3175        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3176            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3177            raise Exception("Incorrect value")
3178
3179        if lots is None or lots < 1:
3180            uLogger.error("You must define trade volume > 0: integer count of lots!")
3181            raise Exception("Incorrect value")
3182
3183        if targetPrice is None or targetPrice <= 0:
3184            uLogger.error("Target price for limit-order must be greater than 0!")
3185            raise Exception("Incorrect value")
3186
3187        if limitPrice is None or limitPrice <= 0:
3188            limitPrice = targetPrice
3189
3190        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3191            stopType = "Limit"
3192
3193        if expDate is None or not expDate:
3194            expDate = "Undefined"
3195
3196        if not (self._ticker or self._figi):
3197            uLogger.error("Tocker or FIGI must be defined!")
3198            raise Exception("Ticker or FIGI required")
3199
3200        response = {}
3201        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3202        self._ticker = instrument["ticker"]
3203        self._figi = instrument["figi"]
3204
3205        if orderType == "Limit":
3206            uLogger.debug(
3207                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3208                    self._ticker, self._figi,
3209                    operation, lots, targetPrice, instrument["currency"],
3210                ))
3211
3212            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3213            self.body = str({
3214                "figi": self._figi,
3215                "quantity": str(lots),
3216                "price": FloatToNano(targetPrice),
3217                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3218                "accountId": str(self.accountId),
3219                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3220            })
3221            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3222
3223            if "orderId" in response.keys():
3224                uLogger.info(
3225                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3226                        response["orderId"], self._ticker, self._figi, operation, lots,
3227                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3228                    ))
3229
3230                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3231                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3232                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3233                            targetPrice, instrument["currency"],
3234                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3235                        ))
3236
3237                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3238                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3239                            targetPrice, instrument["currency"],
3240                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3241                        ))
3242
3243            else:
3244                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3245
3246        if orderType == "Stop":
3247            uLogger.debug(
3248                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3249                    self._ticker, self._figi,
3250                    operation, lots,
3251                    targetPrice, instrument["currency"],
3252                    limitPrice, instrument["currency"],
3253                    stopType, expDate,
3254                ))
3255
3256            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3257            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3258            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3259
3260            body = {
3261                "figi": self._figi,
3262                "quantity": str(lots),
3263                "price": FloatToNano(limitPrice),
3264                "stopPrice": FloatToNano(targetPrice),
3265                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3266                "accountId": str(self.accountId),
3267                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3268                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3269            }
3270
3271            if expDateUTC:
3272                body["expireDate"] = expDateUTC
3273
3274            self.body = str(body)
3275            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3276
3277            if "stopOrderId" in response.keys():
3278                uLogger.info(
3279                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3280                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3281                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3282                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3283                        TKS_STOP_ORDER_TYPES[stopOrderType],
3284                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3285                    ))
3286
3287                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3288                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3289                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3290                            targetPrice, instrument["currency"],
3291                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3292                        ))
3293
3294                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3295                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3296                            targetPrice, instrument["currency"],
3297                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3298                        ))
3299
3300            else:
3301                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3302
3303        return response
3304
3305    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3306        """
3307        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3308        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3309        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3310        See also: `Order()` docstring.
3311
3312        :param lots: volume, integer count of lots >= 1.
3313        :param targetPrice: target price > 0. This is open trade price for limit order.
3314        :return: JSON with response from broker server.
3315        """
3316        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3317
3318    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3319        """
3320        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3321        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3322        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3323        target price value then broker opens a limit order. See also: `Order()` docstring.
3324
3325        :param lots: volume, integer count of lots >= 1.
3326        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3327        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3328                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3329        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3330                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3331        :param expDate: string "Undefined" by default or local date in future.
3332                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3333                        This date is converting to UTC format for server.
3334        :return: JSON with response from broker server.
3335        """
3336        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3337
3338    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3339        """
3340        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3341        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3342        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3343        See also: `Order()` docstring.
3344
3345        :param lots: volume, integer count of lots >= 1.
3346        :param targetPrice: target price > 0. This is open trade price for limit order.
3347        :return: JSON with response from broker server.
3348        """
3349        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3350
3351    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3352        """
3353        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3354        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3355        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3356        target price value then broker opens a limit order. See also: `Order()` docstring.
3357
3358        :param lots: volume, integer count of lots >= 1.
3359        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3360        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3361                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3362        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3363                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3364        :param expDate: string "Undefined" by default or local date in future.
3365                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3366                        This date is converting to UTC format for server.
3367        :return: JSON with response from broker server.
3368        """
3369        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3370
3371    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3372        """
3373        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3374
3375        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3376        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3377                             This avoids unnecessary downloading data from the server.
3378        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3379        """
3380        if self.accountId is None or not self.accountId:
3381            uLogger.error("Variable `accountId` must be defined for using this method!")
3382            raise Exception("Account ID required")
3383
3384        if orderIDs:
3385            if allOrdersIDs is None:
3386                rawOrders = self.RequestPendingOrders()
3387                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3388
3389            if allStopOrdersIDs is None:
3390                rawStopOrders = self.RequestStopOrders()
3391                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3392
3393            for orderID in orderIDs:
3394                idInPendingOrders = orderID in allOrdersIDs
3395                idInStopOrders = orderID in allStopOrdersIDs
3396
3397                if not (idInPendingOrders or idInStopOrders):
3398                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3399                    continue
3400
3401                else:
3402                    if idInPendingOrders:
3403                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3404
3405                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3406                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3407                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3408                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3409
3410                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3411                            if self.moreDebug:
3412                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3413
3414                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3415
3416                        else:
3417                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3418
3419                    elif idInStopOrders:
3420                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3421
3422                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3423                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3424                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3425                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3426
3427                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3428                            if self.moreDebug:
3429                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3430
3431                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3432
3433                        else:
3434                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3435
3436                    else:
3437                        continue
3438
3439    def CloseAllOrders(self) -> None:
3440        """
3441        Gets a list of open pending and stop orders and cancel it all.
3442        """
3443        rawOrders = self.RequestPendingOrders()
3444        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3445        lenOrders = len(allOrdersIDs)
3446
3447        rawStopOrders = self.RequestStopOrders()
3448        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3449        lenSOrders = len(allStopOrdersIDs)
3450
3451        if lenOrders > 0 or lenSOrders > 0:
3452            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3453
3454            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3455
3456        else:
3457            uLogger.info("Orders not found, nothing to cancel.")
3458
3459    def CloseAll(self, *args) -> None:
3460        """
3461        Close all available (not blocked) opened trades and orders.
3462
3463        Also, you can select one or more keywords case-insensitive:
3464        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3465
3466        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3467        """
3468        overview = self.Overview(show=False)  # get all open trades info
3469
3470        if len(args) == 0:
3471            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3472            self.CloseAllOrders()  # close all pending and stop orders
3473
3474            for iType in TKS_INSTRUMENTS:
3475                if iType != "Currencies":
3476                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3477
3478        else:
3479            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3480            lowerArgs = [x.lower() for x in args]
3481
3482            if "orders" in lowerArgs:
3483                self.CloseAllOrders()  # close all pending and stop orders
3484
3485            for iType in TKS_INSTRUMENTS:
3486                if iType.lower() in lowerArgs and iType != "Currencies":
3487                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3488
3489    def CloseAllByTicker(self, instrument: str) -> None:
3490        """
3491        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3492
3493        This method searches opened trade and orders of instrument throw all portfolio and then use
3494        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3495
3496        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3497
3498        :param instrument: string with ticker.
3499        """
3500        if instrument is None or not instrument:
3501            uLogger.error("Ticker name must be defined for using this method!")
3502            raise Exception("Ticker required")
3503
3504        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3505
3506        self._ticker = instrument  # try to set instrument as ticker
3507        self._figi = ""
3508
3509        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3510        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3511
3512        if limitAll and self.IsInLimitOrders(portfolio=overview):
3513            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3514            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3515
3516        if stopAll and self.IsInStopOrders(portfolio=overview):
3517            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3518            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3519
3520        if self.IsInPortfolio(portfolio=overview):
3521            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3522            self.CloseTrades(instruments=[instrument], portfolio=overview)
3523
3524    def CloseAllByFIGI(self, instrument: str) -> None:
3525        """
3526        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3527
3528        This method searches opened trade and orders of instrument throw all portfolio and then use
3529        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3530
3531        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3532
3533        :param instrument: string with FIGI id.
3534        """
3535        if instrument is None or not instrument:
3536            uLogger.error("FIGI id must be defined for using this method!")
3537            raise Exception("FIGI required")
3538
3539        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3540
3541        self._ticker = ""
3542        self._figi = instrument  # try to set instrument as FIGI id
3543
3544        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3545        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3546
3547        if limitAll and self.IsInLimitOrders(portfolio=overview):
3548            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3549            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3550
3551        if stopAll and self.IsInStopOrders(portfolio=overview):
3552            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3553            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3554
3555        if self.IsInPortfolio(portfolio=overview):
3556            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3557            self.CloseTrades(instruments=[instrument], portfolio=overview)
3558
3559    @staticmethod
3560    def ParseOrderParameters(operation, **inputParameters):
3561        """
3562        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3563
3564        :param operation: string "Buy" or "Sell".
3565        :param inputParameters: this is dict of strings that looks like this
3566               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3567               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3568               "prices" key: one or more prices to open limit-orders
3569               Counts of values in lots and prices lists must be equals!
3570        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3571        """
3572        # TODO: update order grid work with api v2
3573        pass
3574        # uLogger.debug("Input parameters: {}".format(inputParameters))
3575        #
3576        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3577        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3578        #     raise Exception("Incorrect value")
3579        #
3580        # if "l" in inputParameters.keys():
3581        #     inputParameters["lots"] = inputParameters.pop("l")
3582        #
3583        # if "p" in inputParameters.keys():
3584        #     inputParameters["prices"] = inputParameters.pop("p")
3585        #
3586        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3587        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3588        #     raise Exception("Incorrect value")
3589        #
3590        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3591        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3592        #
3593        # if len(lots) != len(prices):
3594        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3595        #     raise Exception("Incorrect value")
3596        #
3597        # uLogger.debug("Extracted parameters for orders:")
3598        # uLogger.debug("lots = {}".format(lots))
3599        # uLogger.debug("prices = {}".format(prices))
3600        #
3601        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3602        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3603        # uLogger.debug("Order parameters: {}".format(result))
3604        #
3605        # return result
3606
3607    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3608        """
3609        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3610
3611        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3612        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3613        """
3614        result = False
3615        msg = "Instrument not defined!"
3616
3617        if portfolio is None or not portfolio:
3618            portfolio = self.Overview(show=False)
3619
3620        if self._ticker:
3621            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3622            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3623
3624            for iType in TKS_INSTRUMENTS:
3625                for instrument in portfolio["stat"][iType]:
3626                    if instrument["ticker"] == self._ticker:
3627                        result = True
3628                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3629                        break
3630
3631        elif self._figi:
3632            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3633            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3634
3635            for iType in TKS_INSTRUMENTS:
3636                for instrument in portfolio["stat"][iType]:
3637                    if instrument["figi"] == self._figi:
3638                        result = True
3639                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3640                        break
3641
3642        else:
3643            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3644
3645        uLogger.debug(msg)
3646
3647        return result
3648
3649    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3650        """
3651        Returns instrument from the user's portfolio if it presents there.
3652        Instrument must be defined by `ticker` (highly priority) or `figi`.
3653
3654        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3655        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3656        """
3657        result = None
3658        msg = "Instrument not defined!"
3659
3660        if portfolio is None or not portfolio:
3661            portfolio = self.Overview(show=False)
3662
3663        if self._ticker:
3664            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3665            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3666
3667            for iType in TKS_INSTRUMENTS:
3668                for instrument in portfolio["stat"][iType]:
3669                    if instrument["ticker"] == self._ticker:
3670                        result = instrument
3671                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3672                        break
3673
3674        elif self._figi:
3675            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3676            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3677
3678            for iType in TKS_INSTRUMENTS:
3679                for instrument in portfolio["stat"][iType]:
3680                    if instrument["figi"] == self._figi:
3681                        result = instrument
3682                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3683                        break
3684
3685        else:
3686            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3687
3688        uLogger.debug(msg)
3689
3690        return result
3691
3692    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3693        """
3694        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3695
3696        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3697
3698        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3699        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3700        """
3701        result = False
3702        msg = "Instrument not defined!"
3703
3704        if portfolio is None or not portfolio:
3705            portfolio = self.Overview(show=False)
3706
3707        if self._ticker:
3708            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3709            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3710
3711            for instrument in portfolio["stat"]["orders"]:
3712                if instrument["ticker"] == self._ticker:
3713                    result = True
3714                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3715                    break
3716
3717        elif self._figi:
3718            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3719            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3720
3721            for instrument in portfolio["stat"]["orders"]:
3722                if instrument["figi"] == self._figi:
3723                    result = True
3724                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3725                    break
3726
3727        else:
3728            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3729
3730        uLogger.debug(msg)
3731
3732        return result
3733
3734    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3735        """
3736        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3737        Instrument must be defined by `ticker` (highly priority) or `figi`.
3738
3739        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3740
3741        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3742        :return: list with `orderID`s of limit orders.
3743        """
3744        result = []
3745        msg = "Instrument not defined!"
3746
3747        if portfolio is None or not portfolio:
3748            portfolio = self.Overview(show=False)
3749
3750        if self._ticker:
3751            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3752            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3753
3754            for instrument in portfolio["stat"]["orders"]:
3755                if instrument["ticker"] == self._ticker:
3756                    result.append(instrument["orderID"])
3757
3758            if result:
3759                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3760
3761        elif self._figi:
3762            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3763            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3764
3765            for instrument in portfolio["stat"]["orders"]:
3766                if instrument["figi"] == self._figi:
3767                    result.append(instrument["orderID"])
3768
3769            if result:
3770                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3771
3772        else:
3773            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3774
3775        uLogger.debug(msg)
3776
3777        return result
3778
3779    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3780        """
3781        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3782
3783        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3784
3785        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3786        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3787        """
3788        result = False
3789        msg = "Instrument not defined!"
3790
3791        if portfolio is None or not portfolio:
3792            portfolio = self.Overview(show=False)
3793
3794        if self._ticker:
3795            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3796            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3797
3798            for instrument in portfolio["stat"]["stopOrders"]:
3799                if instrument["ticker"] == self._ticker:
3800                    result = True
3801                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3802                    break
3803
3804        elif self._figi:
3805            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3806            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3807
3808            for instrument in portfolio["stat"]["stopOrders"]:
3809                if instrument["figi"] == self._figi:
3810                    result = True
3811                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3812                    break
3813
3814        else:
3815            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3816
3817        uLogger.debug(msg)
3818
3819        return result
3820
3821    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3822        """
3823        Returns list with all `orderID`s of opened stop orders for the instrument.
3824        Instrument must be defined by `ticker` (highly priority) or `figi`.
3825
3826        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3827
3828        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3829        :return: list with `orderID`s of stop orders.
3830        """
3831        result = []
3832        msg = "Instrument not defined!"
3833
3834        if portfolio is None or not portfolio:
3835            portfolio = self.Overview(show=False)
3836
3837        if self._ticker:
3838            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3839            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3840
3841            for instrument in portfolio["stat"]["stopOrders"]:
3842                if instrument["ticker"] == self._ticker:
3843                    result.append(instrument["orderID"])
3844
3845            if result:
3846                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3847
3848        elif self._figi:
3849            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3850            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3851
3852            for instrument in portfolio["stat"]["stopOrders"]:
3853                if instrument["figi"] == self._figi:
3854                    result.append(instrument["orderID"])
3855
3856            if result:
3857                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3858
3859        else:
3860            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3861
3862        uLogger.debug(msg)
3863
3864        return result
3865
3866    def RequestLimits(self) -> dict:
3867        """
3868        Method for obtaining the available funds for withdrawal for current `accountId`.
3869
3870        See also:
3871        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3872        - `OverviewLimits()` method
3873
3874        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3875                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3876                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3877                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3878        """
3879        if self.accountId is None or not self.accountId:
3880            uLogger.error("Variable `accountId` must be defined for using this method!")
3881            raise Exception("Account ID required")
3882
3883        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3884
3885        self.body = str({"accountId": self.accountId})
3886        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3887        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3888
3889        if self.moreDebug:
3890            uLogger.debug("Records about available funds for withdrawal successfully received")
3891
3892        return rawLimits
3893
3894    def OverviewLimits(self, show: bool = False) -> dict:
3895        """
3896        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3897
3898        See also: `RequestLimits()`.
3899
3900        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3901        :return: dict with raw parsed data from server and some calculated statistics about it.
3902        """
3903        if self.accountId is None or not self.accountId:
3904            uLogger.error("Variable `accountId` must be defined for using this method!")
3905            raise Exception("Account ID required")
3906
3907        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3908
3909        view = {
3910            "rawLimits": rawLimits,
3911            "limits": {  # parsed data for every currency:
3912                "money": {  # this is an array of portfolio currency positions
3913                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3914                },
3915                "blocked": {  # this is an array of blocked currency
3916                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3917                },
3918                "blockedGuarantee": {  # this is locked money under collateral for futures
3919                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3920                },
3921            },
3922        }
3923
3924        # --- Prepare text table with limits in human-readable format:
3925        if show:
3926            info = [
3927                "# Withdrawal limits\n\n",
3928                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3929                "* **Account ID:** [{}]\n".format(self.accountId),
3930            ]
3931
3932            if view["limits"]["money"]:
3933                info.extend([
3934                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3935                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3936                ])
3937
3938            else:
3939                info.append("\nNo withdrawal limits\n")
3940
3941            for curr in view["limits"]["money"].keys():
3942                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3943                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3944                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3945
3946                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3947                    "[{}]".format(curr),
3948                    "{:.2f}".format(view["limits"]["money"][curr]),
3949                    "{:.2f}".format(availableMoney),
3950                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3951                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3952                )
3953
3954                if curr == "rub":
3955                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3956
3957                else:
3958                    info.append(infoStr)
3959
3960            infoText = "".join(info)
3961
3962            uLogger.info(infoText)
3963
3964            if self.withdrawalLimitsFile:
3965                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3966                    fH.write(infoText)
3967
3968                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3969
3970                if self.useHTMLReports:
3971                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3972                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3973                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3974
3975                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3976
3977        return view
3978
3979    def RequestAccounts(self) -> dict:
3980        """
3981        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3982
3983        See also:
3984        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3985        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3986        - `OverviewUserInfo()` method
3987
3988        :return: dict with raw data from server that contains accounts info. Example of dict:
3989                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3990                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3991                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3992                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3993        """
3994        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3995
3996        self.body = str({})
3997        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3998        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3999
4000        if self.moreDebug:
4001            uLogger.debug("Records about available accounts successfully received")
4002
4003        return rawAccounts
4004
4005    def RequestUserInfo(self) -> dict:
4006        """
4007        Method for requesting common user's information.
4008
4009        See also:
4010        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4011        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4012        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4013        - `OverviewUserInfo()` method
4014
4015        :return: dict with raw data from server that contains user's information. Example of dict:
4016                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4017                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4018        """
4019        uLogger.debug("Requesting common user's information. Wait, please...")
4020
4021        self.body = str({})
4022        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4023        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4024
4025        if self.moreDebug:
4026            uLogger.debug("Records about current user successfully received")
4027
4028        return rawUserInfo
4029
4030    def RequestMarginStatus(self, accountId: str = None) -> dict:
4031        """
4032        Method for requesting margin calculation for defined account ID.
4033
4034        See also:
4035        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4036        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4037        - `OverviewUserInfo()` method
4038
4039        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4040        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4041                 Example of responses:
4042                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4043                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4044                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4045                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4046                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4047                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4048        """
4049        if accountId is None or not accountId:
4050            if self.accountId is None or not self.accountId:
4051                uLogger.error("Variable `accountId` must be defined for using this method!")
4052                raise Exception("Account ID required")
4053
4054            else:
4055                accountId = self.accountId  # use `self.accountId` (main ID) by default
4056
4057        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4058
4059        self.body = str({"accountId": accountId})
4060        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4061        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4062
4063        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4064            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4065            rawMargin = {}
4066
4067        else:
4068            if self.moreDebug:
4069                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4070
4071        return rawMargin
4072
4073    def RequestTariffLimits(self) -> dict:
4074        """
4075        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4076
4077        See also:
4078        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4079        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4080        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4081        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4082        - `OverviewUserInfo()` method
4083
4084        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4085                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4086                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4087        """
4088        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4089
4090        self.body = str({})
4091        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4092        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4093
4094        if self.moreDebug:
4095            uLogger.debug("Records with limits of current tariff successfully received")
4096
4097        return rawTariffLimits
4098
4099    def RequestBondCoupons(self, iJSON: dict) -> dict:
4100        """
4101        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4102        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4103        All dates are in UTC timezone.
4104
4105        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4106        Documentation:
4107        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4108        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4109
4110        See also: `ExtendBondsData()`.
4111
4112        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4113                      If raw iJSON is not data of bond then server returns an error [400] with message:
4114                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4115        :return: dictionary with bond payment calendar. Response example
4116                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4117                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4118                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4119                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4120        """
4121        if iJSON["figi"] is None or not iJSON["figi"]:
4122            uLogger.error("FIGI must be defined for using this method!")
4123            raise Exception("FIGI required")
4124
4125        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4126        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4127
4128        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4129            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4130            self._figi,
4131            startDate,
4132            endDate,
4133        ))
4134
4135        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4136        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4137        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4138
4139        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4140            uLogger.warning("Instrument type is not bond!")
4141
4142        else:
4143            if self.moreDebug:
4144                uLogger.debug("Records about bond payment calendar successfully received")
4145
4146        return calendar
4147
4148    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4149        """
4150        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4151        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4152        coupon yields, current yields and some statistics etc.
4153
4154        WARNING! This is too long operation if a lot of bonds requested from broker server.
4155
4156        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4157
4158        :param instruments: list of strings with tickers or FIGIs.
4159        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4160                     for further used by data scientists or stock analytics.
4161        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4162                 In XLSX-file and Pandas DataFrame fields mean:
4163                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4164                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4165        """
4166        if instruments is None or not instruments:
4167            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4168            raise Exception("Ticker or FIGI required")
4169
4170        if isinstance(instruments, str):
4171            instruments = [instruments]
4172
4173        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4174
4175        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4176
4177        iCount = len(uniqueInstruments)
4178        tooLong = iCount >= 20
4179        if tooLong:
4180            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4181
4182        bonds = None
4183        for i, self._figi in enumerate(uniqueInstruments):
4184            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4185
4186            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4187                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4188                rawBond = self.SearchByFIGI(requestPrice=True)
4189
4190                # Widen raw data with UTC current time (iData["actualDateTime"]):
4191                actualDate = datetime.now(tzutc())
4192                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4193
4194                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4195                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4196
4197                # Replace some values with human-readable:
4198                iData["nominalCurrency"] = iData["nominal"]["currency"]
4199                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4200                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4201                iData["aciCurrency"] = iData["aciValue"]["currency"]
4202                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4203                iData["issueSize"] = int(iData["issueSize"])
4204                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4205                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4206                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4207                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4208                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4209                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4210                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4211                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4212                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4213                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4214
4215                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4216                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4217                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4218                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4219                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4220                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4221                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4222                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4223                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4224                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4225                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4226
4227                # Widen raw data with calendar data from `rawCalendar` values:
4228                calendarData = []
4229                if "events" in iData["rawCalendar"].keys():
4230                    for item in iData["rawCalendar"]["events"]:
4231                        calendarData.append({
4232                            "couponDate": item["couponDate"],
4233                            "couponNumber": int(item["couponNumber"]),
4234                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4235                            "payCurrency": item["payOneBond"]["currency"],
4236                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4237                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4238                            "couponStartDate": item["couponStartDate"],
4239                            "couponEndDate": item["couponEndDate"],
4240                            "couponPeriod": item["couponPeriod"],
4241                        })
4242
4243                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4244                    if "maturityDate" not in iData.keys():
4245                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4246
4247                # Widen raw data with Coupon Rate.
4248                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4249                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4250                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4251                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4252
4253                # Widen raw data with Yield to Maturity (YTM) on current date.
4254                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4255                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4256                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4257                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4258                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4259                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4260
4261                iData["calendar"] = calendarData  # adds calendar at the end
4262
4263                # Remove not used data:
4264                iData.pop("uid")
4265                iData.pop("positionUid")
4266                iData.pop("currentPrice")
4267                iData.pop("rawCalendar")
4268
4269                colNames = list(iData.keys())
4270                if bonds is None:
4271                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4272
4273                else:
4274                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4275
4276            else:
4277                uLogger.warning("Instrument is not a bond!")
4278
4279            processed = round(100 * (i + 1) / iCount, 1)
4280            if tooLong and processed % 5 == 0:
4281                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4282
4283            else:
4284                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4285
4286        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4287
4288        # Saving bonds from Pandas DataFrame to XLSX sheet:
4289        if xlsx and self.bondsXLSXFile:
4290            with pd.ExcelWriter(
4291                    path=self.bondsXLSXFile,
4292                    date_format=TKS_DATE_FORMAT,
4293                    datetime_format=TKS_DATE_TIME_FORMAT,
4294                    mode="w",
4295            ) as writer:
4296                bonds.to_excel(
4297                    writer,
4298                    sheet_name="Extended bonds data",
4299                    index=True,
4300                    encoding="UTF-8",
4301                    freeze_panes=(1, 1),
4302                )  # saving as XLSX-file with freeze first row and column as headers
4303
4304            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4305
4306        return bonds
4307
4308    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4309        """
4310        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4311
4312        WARNING! This is too long operation if a lot of bonds requested from broker server.
4313
4314        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4315
4316        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4317                        extended information about bonds: main info, current prices, bond payment calendar,
4318                        coupon yields, current yields and some statistics etc.
4319                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4320        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4321                     for further used by data scientists or stock analytics.
4322        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4323        """
4324        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4325            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4326
4327        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4328
4329        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4330        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4331        calendar = None
4332        for bond in extBonds.iterrows():
4333            for item in bond[1]["calendar"]:
4334                cData = {
4335                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4336                    "couponDate": item["couponDate"],
4337                    "figi": bond[1]["figi"],
4338                    "ticker": bond[1]["ticker"],
4339                    "name": bond[1]["name"],
4340                    "couponNumber": item["couponNumber"],
4341                    "payOneBond": item["payOneBond"],
4342                    "payCurrency": item["payCurrency"],
4343                    "couponType": item["couponType"],
4344                    "couponPeriod": item["couponPeriod"],
4345                    "fixDate": item["fixDate"],
4346                    "couponStartDate": item["couponStartDate"],
4347                    "couponEndDate": item["couponEndDate"],
4348                }
4349
4350                if calendar is None:
4351                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4352
4353                else:
4354                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4355
4356        if calendar is not None:
4357            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4358
4359            # Saving calendar from Pandas DataFrame to XLSX sheet:
4360            if xlsx:
4361                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4362
4363                with pd.ExcelWriter(
4364                        path=xlsxCalendarFile,
4365                        date_format=TKS_DATE_FORMAT,
4366                        datetime_format=TKS_DATE_TIME_FORMAT,
4367                        mode="w",
4368                ) as writer:
4369                    humanReadable = calendar.copy(deep=True)
4370                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4371                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4372                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4373                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4374                    humanReadable.columns = colNames  # human-readable column names
4375
4376                    humanReadable.to_excel(
4377                        writer,
4378                        sheet_name="Bond payments calendar",
4379                        index=False,
4380                        encoding="UTF-8",
4381                        freeze_panes=(1, 2),
4382                    )  # saving as XLSX-file with freeze first row and column as headers
4383
4384                    del humanReadable  # release df in memory
4385
4386                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4387
4388        return calendar
4389
4390    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4391        """
4392        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4393        Also, creates Markdown file with calendar data, `calendar.md` by default.
4394
4395        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4396
4397        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4398                        extended information about bonds: main info, current prices, bond payment calendar,
4399                        coupon yields, current yields and some statistics etc.
4400                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4401        :param show: if `True` then also printing bonds payment calendar to the console,
4402                     otherwise save to file `calendarFile` only. `False` by default.
4403        :return: multilines text in Markdown format with bonds payment calendar as a table.
4404        """
4405        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4406            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4407
4408        infoText = "# Bond payments calendar\n\n"
4409
4410        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4411
4412        if not (calendar is None or calendar.empty):
4413            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4414
4415            info = [
4416                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4417                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4418                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4419            ]
4420
4421            newMonth = False
4422            notOneBond = calendar["figi"].nunique() > 1
4423            for i, bond in enumerate(calendar.iterrows()):
4424                if newMonth and notOneBond:
4425                    info.append(splitLine)
4426
4427                info.append(
4428                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4429                        "  √" if bond[1]["paid"] else "  —",
4430                        bond[1]["couponDate"].split("T")[0],
4431                        bond[1]["figi"],
4432                        bond[1]["ticker"],
4433                        bond[1]["couponNumber"],
4434                        "{} {}".format(
4435                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4436                            bond[1]["payCurrency"],
4437                        ),
4438                        bond[1]["couponType"],
4439                        bond[1]["couponPeriod"],
4440                        bond[1]["fixDate"].split("T")[0],
4441                    )
4442                )
4443
4444                if i < len(calendar.values) - 1:
4445                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4446                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4447                    newMonth = False if curDate.month == nextDate.month else True
4448
4449                else:
4450                    newMonth = False
4451
4452            infoText += "".join(info)
4453
4454            if show:
4455                uLogger.info("{}".format(infoText))
4456
4457            if self.calendarFile is not None:
4458                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4459                    fH.write(infoText)
4460
4461                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4462
4463                if self.useHTMLReports:
4464                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4465                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4466                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4467
4468                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4469
4470        else:
4471            infoText += "No data\n"
4472
4473        return infoText
4474
4475    def OverviewAccounts(self, show: bool = False) -> dict:
4476        """
4477        Method for parsing and show simple table with all available user accounts.
4478
4479        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4480
4481        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4482        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4483                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4484                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4485                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4486                                                        "closed": "—", "access": "Full access" }, ...}}`
4487        """
4488        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4489
4490        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4491        accounts = {
4492            item["id"]: {
4493                "type": TKS_ACCOUNT_TYPES[item["type"]],
4494                "name": item["name"],
4495                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4496                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4497                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4498                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4499            } for item in rawAccounts["accounts"]
4500        }
4501
4502        # Raw and parsed data with some fields replaced in "stat" section:
4503        view = {
4504            "rawAccounts": rawAccounts,
4505            "stat": accounts,
4506        }
4507
4508        # --- Prepare simple text table with only accounts data in human-readable format:
4509        if show:
4510            info = [
4511                "# User accounts\n\n",
4512                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4513                "| Account ID   | Type                      | Status                    | Name                           |\n",
4514                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4515            ]
4516
4517            for account in view["stat"].keys():
4518                info.extend([
4519                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4520                        account,
4521                        view["stat"][account]["type"],
4522                        view["stat"][account]["status"],
4523                        view["stat"][account]["name"],
4524                    )
4525                ])
4526
4527            infoText = "".join(info)
4528
4529            uLogger.info(infoText)
4530
4531            if self.userAccountsFile:
4532                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4533                    fH.write(infoText)
4534
4535                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4536
4537                if self.useHTMLReports:
4538                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4539                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4540                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4541
4542                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4543
4544        return view
4545
4546    def OverviewUserInfo(self, show: bool = False) -> dict:
4547        """
4548        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4549
4550        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4551
4552        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4553        :return: dict with raw parsed data from server and some calculated statistics about it.
4554        """
4555        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4556        tmpTicker = self._ticker
4557        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4558        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4559        self._ticker = tmpTicker
4560
4561        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4562        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4563        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4564        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4565        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4566        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4567
4568        # This is dict with parsed common user data:
4569        userInfo = {
4570            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4571            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4572            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4573            "tariff": rawUserInfo["tariff"],
4574        }
4575
4576        # This is an array of dict with parsed margin statuses for every account IDs:
4577        margins = {}
4578        for accountId in accounts.keys():
4579            if rawMargins[accountId]:
4580                margins[accountId] = {
4581                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4582                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4583                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4584                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4585                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4586                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4587                    "missing": missing["volume"],
4588                }
4589
4590            else:
4591                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4592
4593        unary = {}  # unary-connection limits
4594        for item in rawTariffLimits["unaryLimits"]:
4595            if item["limitPerMinute"] in unary.keys():
4596                unary[item["limitPerMinute"]].extend(item["methods"])
4597
4598            else:
4599                unary[item["limitPerMinute"]] = item["methods"]
4600
4601        stream = {}  # stream-connection limits
4602        for item in rawTariffLimits["streamLimits"]:
4603            if item["limit"] in stream.keys():
4604                stream[item["limit"]].extend(item["streams"])
4605
4606            else:
4607                stream[item["limit"]] = item["streams"]
4608
4609        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4610        limits = {
4611            "unary": unary,
4612            "stream": stream,
4613        }
4614
4615        # Raw and parsed data as an output result:
4616        view = {
4617            "rawUserInfo": rawUserInfo,
4618            "rawAccounts": rawAccounts,
4619            "rawMargins": rawMargins,
4620            "rawTariffLimits": rawTariffLimits,
4621            "stat": {
4622                "overview": overview,
4623                "userInfo": userInfo,
4624                "accounts": accounts,
4625                "margins": margins,
4626                "limits": limits,
4627            },
4628        }
4629
4630        # --- Prepare text table with user information in human-readable format:
4631        if show:
4632            info = [
4633                "# Full user information\n\n",
4634                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4635                "## Common information\n\n",
4636                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4637                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4638                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4639                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4640                "\n## User accounts\n\n",
4641            ]
4642
4643            for account in view["stat"]["accounts"].keys():
4644                info.extend([
4645                    "### ID: [{}]\n\n".format(account),
4646                    "| Parameters           | Values                                                       |\n",
4647                    "|----------------------|--------------------------------------------------------------|\n",
4648                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4649                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4650                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4651                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4652                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4653                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4654                ])
4655
4656                if margins[account]:
4657                    info.extend([
4658                        "| Margin status:       | Enabled                                                      |\n",
4659                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4660                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4661                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4662                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4663                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4664                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4665                    ])
4666
4667                else:
4668                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4669
4670            info.extend([
4671                "\n## Current user tariff limits\n",
4672                "\n### See also\n",
4673                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4674                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4675                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4676                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4677                "\n### Unary limits\n",
4678            ])
4679
4680            if unary:
4681                for key, values in sorted(unary.items()):
4682                    info.append("\n* Max requests per minute: {}\n".format(key))
4683
4684                    for value in values:
4685                        info.append("  - {}\n".format(value))
4686
4687            else:
4688                info.append("\nNot available\n")
4689
4690            info.append("\n### Stream limits\n")
4691
4692            if stream:
4693                for key, values in sorted(stream.items()):
4694                    info.append("\n* Max stream connections: {}\n".format(key))
4695
4696                    for value in values:
4697                        info.append("  - {}\n".format(value))
4698
4699            else:
4700                info.append("\nNot available\n")
4701
4702            infoText = "".join(info)
4703
4704            uLogger.info(infoText)
4705
4706            if self.userInfoFile:
4707                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4708                    fH.write(infoText)
4709
4710                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4711
4712                if self.useHTMLReports:
4713                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4714                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4715                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4716
4717                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4718
4719        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.__lock = Lock()  # initialize multiprocessing mutex lock
130
131        self.aliases = TKS_TICKER_ALIASES
132        """Some aliases instead official tickers.
133
134        See also: `TKSEnums.TKS_TICKER_ALIASES`
135        """
136
137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
138
139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
140
141        self._ticker = ""
142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
143
144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
146
147        See also: `SearchByTicker()`, `SearchInstruments()`.
148        """
149
150        self._figi = ""
151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
152
153        See also: `SearchByFIGI()`, `SearchInstruments()`.
154        """
155
156        self.depth = 1
157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
158
159        See also: `GetCurrentPrices()`.
160        """
161
162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
164
165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
166        """
167
168        uLogger.debug("Broker API server: {}".format(self.server))
169
170        self.timeout = 15
171        """Server operations timeout in seconds. Default: `15`.
172
173        See also: `SendAPIRequest()`.
174        """
175
176        self.headers = {
177            "Content-Type": "application/json",
178            "accept": "application/json",
179            "Authorization": "Bearer {}".format(self.token),
180            "x-app-name": "Tim55667757.TKSBrokerAPI",
181        }
182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
183
184        See also: `SendAPIRequest()`.
185        """
186
187        self.body = None
188        """Request body which send to broker server. Default: `None`.
189
190        See also: `SendAPIRequest()`.
191        """
192
193        self.moreDebug = False
194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
195
196        self.useHTMLReports = False
197        """
198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
199        
200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
201        """
202
203        self.historyFile = None
204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
205
206        See also: `History()`.
207        """
208
209        self.htmlHistoryFile = "index.html"
210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
211
212        See also: `ShowHistoryChart()`.
213        """
214
215        self.instrumentsFile = "instruments.md"
216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
217
218        See also: `ShowInstrumentsInfo()`.
219        """
220
221        self.searchResultsFile = "search-results.md"
222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
223
224        See also: `SearchInstruments()`.
225        """
226
227        self.pricesFile = "prices.md"
228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
229
230        See also: `GetListOfPrices()`.
231        """
232
233        self.infoFile = "info.md"
234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
235
236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
237        """
238
239        self.bondsXLSXFile = "ext-bonds.xlsx"
240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
242
243        See also: `ExtendBondsData()`.
244        """
245
246        self.calendarFile = "calendar.md"
247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
248        
249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
250
251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
252        """
253
254        self.overviewFile = "overview.md"
255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
256
257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
258        """
259
260        self.overviewDigestFile = "overview-digest.md"
261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
262
263        See also: `Overview()` with parameter `details="digest"`.
264        """
265
266        self.overviewPositionsFile = "overview-positions.md"
267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
268
269        See also: `Overview()` with parameter `details="positions"`.
270        """
271
272        self.overviewOrdersFile = "overview-orders.md"
273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
274
275        See also: `Overview()` with parameter `details="orders"`.
276        """
277
278        self.overviewAnalyticsFile = "overview-analytics.md"
279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
280
281        See also: `Overview()` with parameter `details="analytics"`.
282        """
283
284        self.overviewBondsCalendarFile = "overview-calendar.md"
285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
286
287        See also: `Overview()` with parameter `details="calendar"`.
288        """
289
290        self.reportFile = "deals.md"
291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
292
293        See also: `Deals()`.
294        """
295
296        self.withdrawalLimitsFile = "limits.md"
297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
298
299        See also: `OverviewLimits()` and `RequestLimits()`.
300        """
301
302        self.userInfoFile = "user-info.md"
303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
304
305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
306        """
307
308        self.userAccountsFile = "accounts.md"
309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
310
311        See also: `OverviewAccounts()`, `RequestAccounts()`.
312        """
313
314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
316
317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
318
319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
320        """
321
322        self.iList = None  # init iList for raw instruments data
323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
324        
325        See also: `Listing()`, `DumpInstruments()`.
326        """
327
328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
329        if useCache:
330            if os.path.exists(self.iListDumpFile):
331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
332                curTime = datetime.now(tzutc())
333
334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
336
337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
338
339                else:
340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
341
342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
343                        os.path.abspath(self.iListDumpFile),
344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
345                    ))
346
347            else:
348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
350
351        else:
352            self.iList = self.Listing()  # request new raw instruments data from broker server
353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
354
355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
357
358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
359        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
413    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
414        """
415        Send GET or POST request to broker server and receive JSON object.
416
417        self.header: must be defining with dictionary of headers.
418        self.body: if define then used as request body. None by default.
419        self.timeout: global request timeout, 15 seconds by default.
420        :param url: url with REST request.
421        :param reqType: send "GET" or "POST" request. "GET" by default.
422        :param retry: how many times retry after first request if an 5xx server errors occurred.
423        :param pause: sleep time in seconds between retries.
424        :return: response JSON (dictionary) from broker.
425        """
426        if reqType.upper() not in ("GET", "POST"):
427            uLogger.error("You can define request type: `GET` or `POST`!")
428            raise Exception("Incorrect value")
429
430        if self.moreDebug:
431            uLogger.debug("Request parameters:")
432            uLogger.debug("    - REST API URL: {}".format(url))
433            uLogger.debug("    - request type: {}".format(reqType))
434            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
435            uLogger.debug("    - body:\n{}".format(self.body))
436
437        # fast hack to avoid all operations with some tickers/FIGI
438        responseJSON = {}
439        oK = True
440        for item in self.exclude:
441            if item in url:
442                if self.moreDebug:
443                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
444
445                oK = False
446                break
447
448        if oK:
449            with self.__lock:  # acquire the mutex lock
450                counter = 0
451                response = None
452                errMsg = ""
453
454                while not response and counter <= retry:
455                    if reqType == "GET":
456                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
457
458                    if reqType == "POST":
459                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
460
461                    if self.moreDebug:
462                        uLogger.debug("Response:")
463                        uLogger.debug("    - status code: {}".format(response.status_code))
464                        uLogger.debug("    - reason: {}".format(response.reason))
465                        uLogger.debug("    - body length: {}".format(len(response.text)))
466                        uLogger.debug("    - headers:\n{}".format(response.headers))
467
468                    # Server returns some headers:
469                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
470                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
471                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
472                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
473                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
474                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
475                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
476                        sleep(rateLimitWait)
477
478                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
479                    if 400 <= response.status_code < 500:
480                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
481                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
482
483                        if "code" in response.text and "message" in response.text:
484                            msgDict = self._ParseJSON(rawData=response.text)
485                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
486
487                        counter = retry + 1  # do not retry for 4xx errors
488
489                    if 500 <= response.status_code < 600:
490                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
491                        uLogger.debug("    - not oK, {}".format(errMsg))
492
493                        if "code" in response.text and "message" in response.text:
494                            errMsgDict = self._ParseJSON(rawData=response.text)
495                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
496
497                        counter += 1
498
499                        if counter <= retry:
500                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
501                            sleep(pause)
502
503                responseJSON = self._ParseJSON(rawData=response.text)
504
505                if errMsg:
506                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
507                    uLogger.error("    - not oK, {}".format(errMsg))
508
509        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
542    def Listing(self) -> dict:
543        """
544        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
545
546        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
547        """
548        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
549        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
550
551        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
552        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
553        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
554
555        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
556        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
557        poolUpdater.close()  # close the thread pool
558        poolUpdater.join()  # wait a moment until all data returns from threads
559
560        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
561        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
562        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
563
564        # calculate minimum price increment (step) for all instruments and set up instrument's type:
565        for iType in iList.keys():
566            for ticker in iList[iType]:
567                iList[iType][ticker]["type"] = iType
568
569                if "minPriceIncrement" in iList[iType][ticker].keys():
570                    iList[iType][ticker]["step"] = NanoToFloat(
571                        iList[iType][ticker]["minPriceIncrement"]["units"],
572                        iList[iType][ticker]["minPriceIncrement"]["nano"],
573                    )
574
575                else:
576                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
577
578        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
580    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
581        """
582        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
583
584        See also: `DumpInstruments()`, `Listing()`.
585
586        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
587                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
588        """
589        if self.iListDumpFile is None or not self.iListDumpFile:
590            uLogger.error("Output name of dump file must be defined!")
591            raise Exception("Filename required")
592
593        if not self.iList or forceUpdate:
594            self.iList = self.Listing()
595
596        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
597
598        # Save as XLSX with separated sheets for every type of instruments:
599        with pd.ExcelWriter(
600                path=xlsxDumpFile,
601                date_format=TKS_DATE_FORMAT,
602                datetime_format=TKS_DATE_TIME_FORMAT,
603                mode="w",
604        ) as writer:
605            for iType in TKS_INSTRUMENTS:
606                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
607                df = df[sorted(df)]  # sorted by column names
608                df = df.applymap(
609                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
610                    na_action="ignore",
611                )  # converting numbers from nano-type to float in every cell
612                df.to_excel(
613                    writer,
614                    sheet_name=iType,
615                    encoding="UTF-8",
616                    freeze_panes=(1, 1),
617                )  # saving as XLSX-file with freeze first row and column as headers
618
619        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
621    def DumpInstruments(self, forceUpdate: bool = True) -> str:
622        """
623        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
624        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
625
626        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
627
628        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
629                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
630        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
631        """
632        if self.iListDumpFile is None or not self.iListDumpFile:
633            uLogger.error("Output name of dump file must be defined!")
634            raise Exception("Filename required")
635
636        if not self.iList or forceUpdate:
637            self.iList = self.Listing()
638
639        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
640        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
641            fH.write(jsonDump)
642
643        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
644
645        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
647    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
648        """
649        Show information about one instrument defined by json data and prints it in Markdown format.
650
651        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
652
653        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
654        :param show: if `True` then also printing information about instrument and its current price.
655        :return: multilines text in Markdown format with information about one instrument.
656        """
657        splitLine = "|                                                             |                                                        |\n"
658        infoText = ""
659
660        if iJSON is not None and iJSON and isinstance(iJSON, dict):
661            info = [
662                "# Main information\n\n",
663                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
664                "| Parameters                                                  | Values                                                 |\n",
665                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
666                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
667                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
668            ]
669
670            if "sector" in iJSON.keys() and iJSON["sector"]:
671                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
672
673            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
674                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
675
676            info.extend([
677                splitLine,
678                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
679                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
680            ])
681
682            if "isin" in iJSON.keys() and iJSON["isin"]:
683                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
684
685            if "classCode" in iJSON.keys():
686                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
687
688            info.extend([
689                splitLine,
690                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
691                splitLine,
692                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
693                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
694                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
695            ])
696
697            if iJSON["figi"]:
698                self._figi = iJSON["figi"]
699                iJSON = iJSON | self.RequestTradingStatus()
700
701                info.extend([
702                    splitLine,
703                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
704                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
705                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
706                ])
707
708            info.append(splitLine)
709
710            if "type" in iJSON.keys() and iJSON["type"]:
711                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
712
713                if "shareType" in iJSON.keys() and iJSON["shareType"]:
714                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
715
716            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
717                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
718
719            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
720                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
721
722            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
723                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
724
725            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
726                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
727
728            if "focusType" in iJSON.keys() and iJSON["focusType"]:
729                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
730
731            if "assetType" in iJSON.keys() and iJSON["assetType"]:
732                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
733
734            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
735                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
736
737            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
738                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
739
740            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
741                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
742
743            if "currency" in iJSON.keys():
744                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
745
746            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
747                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
748
749            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
750                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
751
752            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
753                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
754
755            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
756                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
757
758            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
759                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
760
761            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
762                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
763
764            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
765                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
766
767            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
768                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
769
770            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
771                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
772
773            iExt = None
774            if iJSON["type"] == "Bonds":
775                info.extend([
776                    splitLine,
777                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
778                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
779                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
780                        iJSON["nominal"]["currency"],
781                    )),
782                ])
783
784                if "floatingCouponFlag" in iJSON.keys():
785                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
786
787                if "amortizationFlag" in iJSON.keys():
788                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
789
790                info.append(splitLine)
791
792                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
793                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
794
795                if iJSON["figi"]:
796                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
797
798                    info.extend([
799                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
800                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
801                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
802                    ])
803
804                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
805                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
806                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
807                        iJSON["aciValue"]["currency"]
808                    )))
809
810            if "currentPrice" in iJSON.keys():
811                info.append(splitLine)
812
813                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
814                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
815
816                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
817                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
818                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
819                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
820                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
821
822                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
823                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
824
825                info.extend([
826                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
827                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
828                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
829                    )),
830                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
831                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
832                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
833                    )),
834                    "| Changes between last deal price and last close              | {:<54} |\n".format(
835                        "{:.2f}%{}".format(
836                            iJSON["currentPrice"]["changes"],
837                            " ({}{:.2f} {})".format(
838                                "+" if bondChangesDelta > 0 else "",
839                                bondChangesDelta,
840                                aciCurrency
841                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
842                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
843                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
844                                currency
845                            ),
846                        )
847                    ),
848                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
849                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
850                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
851                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
852                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
853                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
854                    )),
855                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
856                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
858                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
859                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
860                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
861                    )),
862                ])
863
864            if "lot" in iJSON.keys():
865                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
866
867            if "step" in iJSON.keys() and iJSON["step"] != 0:
868                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
869
870            # Add bond payment calendar:
871            if iJSON["type"] == "Bonds":
872                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
873                info.extend(["\n#", strCalendar])
874
875            infoText += "".join(info)
876
877            if show:
878                uLogger.info("{}".format(infoText))
879
880            else:
881                uLogger.debug("{}".format(infoText))
882
883            if self.infoFile is not None:
884                with open(self.infoFile, "w", encoding="UTF-8") as fH:
885                    fH.write(infoText)
886
887                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
888
889                if self.useHTMLReports:
890                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
891                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
892                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
893
894                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
895
896        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
898    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
899        """
900        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
901
902        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
903        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
904        :return: JSON formatted data with information about instrument.
905        """
906        tickerJSON = {}
907        if self.moreDebug:
908            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
909
910        if not self._ticker:
911            uLogger.warning("self._ticker variable is not be empty!")
912
913        else:
914            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
915                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
916                raise Exception("Instrument not allowed")
917
918            if not self.iList:
919                self.iList = self.Listing()
920
921            if self._ticker in self.iList["Shares"].keys():
922                tickerJSON = self.iList["Shares"][self._ticker]
923                if self.moreDebug:
924                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
925
926            elif self._ticker in self.iList["Currencies"].keys():
927                tickerJSON = self.iList["Currencies"][self._ticker]
928                if self.moreDebug:
929                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
930
931            elif self._ticker in self.iList["Bonds"].keys():
932                tickerJSON = self.iList["Bonds"][self._ticker]
933                if self.moreDebug:
934                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
935
936            elif self._ticker in self.iList["Etfs"].keys():
937                tickerJSON = self.iList["Etfs"][self._ticker]
938                if self.moreDebug:
939                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
940
941            elif self._ticker in self.iList["Futures"].keys():
942                tickerJSON = self.iList["Futures"][self._ticker]
943                if self.moreDebug:
944                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
945
946        if tickerJSON:
947            self._figi = tickerJSON["figi"]
948
949            if requestPrice:
950                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
951
952                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
953                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
954
955                else:
956                    tickerJSON["currentPrice"]["changes"] = 0
957
958            if show:
959                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
960
961        else:
962            if show:
963                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
964
965        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 967    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 968        """
 969        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 970
 971        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 972        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 973        :return: JSON formatted data with information about instrument.
 974        """
 975        figiJSON = {}
 976        if self.moreDebug:
 977            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 978
 979        if not self._figi:
 980            uLogger.warning("self._figi variable is not be empty!")
 981
 982        else:
 983            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 984                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 985                raise Exception("Instrument not allowed")
 986
 987            if not self.iList:
 988                self.iList = self.Listing()
 989
 990            for item in self.iList["Shares"].keys():
 991                if self._figi == self.iList["Shares"][item]["figi"]:
 992                    figiJSON = self.iList["Shares"][item]
 993
 994                    if self.moreDebug:
 995                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 996
 997                    break
 998
 999            if not figiJSON:
1000                for item in self.iList["Currencies"].keys():
1001                    if self._figi == self.iList["Currencies"][item]["figi"]:
1002                        figiJSON = self.iList["Currencies"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Bonds"].keys():
1011                    if self._figi == self.iList["Bonds"][item]["figi"]:
1012                        figiJSON = self.iList["Bonds"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1016
1017                        break
1018
1019            if not figiJSON:
1020                for item in self.iList["Etfs"].keys():
1021                    if self._figi == self.iList["Etfs"][item]["figi"]:
1022                        figiJSON = self.iList["Etfs"][item]
1023
1024                        if self.moreDebug:
1025                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1026
1027                        break
1028
1029            if not figiJSON:
1030                for item in self.iList["Futures"].keys():
1031                    if self._figi == self.iList["Futures"][item]["figi"]:
1032                        figiJSON = self.iList["Futures"][item]
1033
1034                        if self.moreDebug:
1035                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1036
1037                        break
1038
1039        if figiJSON:
1040            self._figi = figiJSON["figi"]
1041            self._ticker = figiJSON["ticker"]
1042
1043            if requestPrice:
1044                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1045
1046                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1047                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1048
1049                else:
1050                    figiJSON["currentPrice"]["changes"] = 0
1051
1052            if show:
1053                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1054
1055        else:
1056            if show:
1057                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1058
1059        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1061    def GetCurrentPrices(self, show: bool = True) -> dict:
1062        """
1063        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1064        `{"buy": [{"price": 1243.8, "quantity": 193},
1065                  {"price": 1244.0, "quantity": 168},
1066                  {"price": 1244.8, "quantity": 5},
1067                  {"price": 1245.0, "quantity": 61},
1068                  {"price": 1245.4, "quantity": 60}],
1069          "sell": [{"price": 1243.6, "quantity": 8},
1070                   {"price": 1242.6, "quantity": 10},
1071                   {"price": 1242.4, "quantity": 18},
1072                   {"price": 1242.2, "quantity": 50},
1073                   {"price": 1242.0, "quantity": 113}],
1074          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1075        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1076        - sell: list of dicts with Buyers prices,
1077            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1078            - quantity: volume value by current price in lots,
1079        - limitUp: current trade session limit price, maximum,
1080        - limitDown: current trade session limit price, minimum,
1081        - lastPrice: last deal price of the instrument,
1082        - closePrice: previous trade session close price of the instrument.
1083
1084        See also: `SearchByTicker()` and `SearchByFIGI()`.
1085        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1086        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1087
1088        :param show: if `True` then print DOM to log and console.
1089        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1090                 If an error occurred then returns an empty record:
1091                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1092        """
1093        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1094
1095        if self.depth < 1:
1096            uLogger.error("Depth of Market (DOM) must be >=1!")
1097            raise Exception("Incorrect value")
1098
1099        if not (self._ticker or self._figi):
1100            uLogger.error("self._ticker or self._figi variables must be defined!")
1101            raise Exception("Ticker or FIGI required")
1102
1103        if self._ticker and not self._figi:
1104            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1106
1107        if not self._ticker and self._figi:
1108            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1110
1111        if not self._figi:
1112            uLogger.error("FIGI is not defined!")
1113            raise Exception("Ticker or FIGI required")
1114
1115        else:
1116            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1117
1118            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1120            self.body = str({"figi": self._figi, "depth": self.depth})
1121            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1124                # list of dicts with sellers orders:
1125                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1126
1127                # list of dicts with buyers orders:
1128                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1129
1130                # max price of instrument at this time:
1131                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1132
1133                # min price of instrument at this time:
1134                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1135
1136                # last price of deal with instrument:
1137                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1138
1139                # last close price of instrument:
1140                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1141
1142            else:
1143                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1144                uLogger.debug("Server response: {}".format(pricesResponse))
1145
1146            if show:
1147                if prices["buy"] or prices["sell"]:
1148                    info = [
1149                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1150                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1151                            self._ticker,
1152                            self._figi,
1153                            self.depth,
1154                        ),
1155                        "-" * 60, "\n",
1156                        "             Orders of Buyers | Orders of Sellers\n",
1157                        "-" * 60, "\n",
1158                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1159                        "-" * 60, "\n",
1160                    ]
1161
1162                    if not prices["buy"]:
1163                        info.append("                              | No orders!\n")
1164                        sumBuy = 0
1165
1166                    else:
1167                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1168                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1169                        for item in maxMinSorted:
1170                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1171
1172                    if not prices["sell"]:
1173                        info.append("No orders!                    |\n")
1174                        sumSell = 0
1175
1176                    else:
1177                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1178                        for item in prices["sell"]:
1179                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1180
1181                    info.extend([
1182                        "-" * 60, "\n",
1183                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1184                        "-" * 60, "\n",
1185                    ])
1186
1187                    infoText = "".join(info)
1188
1189                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1190
1191                else:
1192                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1193
1194        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1196    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1197        """
1198        This method get and show information about all available broker instruments for current user account.
1199        If `instrumentsFile` string is not empty then also save information to this file.
1200
1201        :param show: if `True` then print results to console, if `False` — print only to file.
1202        :return: multi-lines string with all available broker instruments
1203        """
1204        if not self.iList:
1205            self.iList = self.Listing()
1206
1207        info = [
1208            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1209            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1210        ]
1211
1212        # add instruments count by type:
1213        for iType in self.iList.keys():
1214            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1215
1216        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1217        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1218
1219        # generating info tables with all instruments by type:
1220        for iType in self.iList.keys():
1221            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1222
1223            for instrument in self.iList[iType].keys():
1224                iName = self.iList[iType][instrument]["name"]  # instrument's name
1225                if len(iName) > 57:
1226                    iName = "{}...".format(iName[:54])  # right trim for a long string
1227
1228                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1229                    self.iList[iType][instrument]["ticker"],
1230                    iName,
1231                    self.iList[iType][instrument]["figi"],
1232                    self.iList[iType][instrument]["currency"],
1233                    self.iList[iType][instrument]["lot"],
1234                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1235                ))
1236
1237        infoText = "".join(info)
1238
1239        if show:
1240            uLogger.info(infoText)
1241
1242        if self.instrumentsFile:
1243            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1244                fH.write(infoText)
1245
1246            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1247
1248            if self.useHTMLReports:
1249                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1250                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1251                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1252
1253                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1254
1255        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1257    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1258        """
1259        This method search and show information about instruments by part of its ticker, FIGI or name.
1260        If `searchResultsFile` string is not empty then also save information to this file.
1261
1262        :param pattern: string with part of ticker, FIGI or instrument's name.
1263        :param show: if `True` then print results to console, if `False` — return list of result only.
1264        :return: list of dictionaries with all found instruments.
1265        """
1266        if not self.iList:
1267            self.iList = self.Listing()
1268
1269        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1270        compiledPattern = re.compile(pattern, re.IGNORECASE)
1271
1272        for iType in self.iList:
1273            for instrument in self.iList[iType].values():
1274                searchResult = compiledPattern.search(" ".join(
1275                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1276                ))
1277
1278                if searchResult:
1279                    searchResults[iType][instrument["ticker"]] = instrument
1280
1281        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1282        info = [
1283            "# Search results\n\n",
1284            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1285            "* **Search pattern:** [{}]\n".format(pattern),
1286            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1287            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1288        ]
1289        infoShort = info[:]
1290
1291        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1292        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1293        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1294
1295        if resultsLen == 0:
1296            info.append("\nNo results\n")
1297            infoShort.append("\nNo results\n")
1298            uLogger.warning("No results. Try changing your search pattern.")
1299
1300        else:
1301            for iType in searchResults:
1302                iTypeValuesCount = len(searchResults[iType].values())
1303                if iTypeValuesCount > 0:
1304                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1306
1307                    for instrument in searchResults[iType].values():
1308                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1309                            instrument["type"],
1310                            instrument["ticker"],
1311                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1312                            instrument["figi"],
1313                        ))
1314
1315                    if iTypeValuesCount <= 5:
1316                        infoShort.extend(info[-iTypeValuesCount:])
1317
1318                    else:
1319                        infoShort.extend(info[-5:])
1320                        infoShort.append(skippedLine)
1321
1322        infoText = "".join(info)
1323        infoTextShort = "".join(infoShort)
1324
1325        if show:
1326            uLogger.info(infoTextShort)
1327            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1328
1329        if self.searchResultsFile:
1330            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1331                fH.write(infoText)
1332
1333            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1334
1335            if self.useHTMLReports:
1336                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1337                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1338                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1339
1340                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1341
1342        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1345        """
1346        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1347
1348        :param instruments: list of strings with tickers or FIGIs.
1349        :return: list with unique instrument FIGIs only.
1350        """
1351        requestedInstruments = []
1352        for iName in instruments:
1353            if iName not in self.aliases.keys():
1354                if iName not in requestedInstruments:
1355                    requestedInstruments.append(iName)
1356
1357            else:
1358                if iName not in requestedInstruments:
1359                    if self.aliases[iName] not in requestedInstruments:
1360                        requestedInstruments.append(self.aliases[iName])
1361
1362        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1363
1364        onlyUniqueFIGIs = []
1365        for iName in requestedInstruments:
1366            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1367                continue
1368
1369            self._ticker = iName
1370            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1371
1372            if not iData:
1373                self._ticker = ""
1374                self._figi = iName
1375
1376                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1377
1378                if not iData:
1379                    self._figi = ""
1380                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1381
1382            if iData and iData["figi"] not in onlyUniqueFIGIs:
1383                onlyUniqueFIGIs.append(iData["figi"])
1384
1385        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1386
1387        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1389    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1390        """
1391        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1392
1393        See limits: https://tinkoff.github.io/investAPI/limits/
1394
1395        If `pricesFile` string is not empty then also save information to this file.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1399        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1400                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1401        """
1402        if instruments is None or not instruments:
1403            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1404            raise Exception("Ticker or FIGI required")
1405
1406        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1407
1408        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1409
1410        iList = []  # trying to get info and current prices about all unique instruments:
1411        for self._figi in onlyUniqueFIGIs:
1412            iData = self.SearchByFIGI(requestPrice=True)
1413            iList.append(iData)
1414
1415        self.ShowListOfPrices(iList, show)
1416
1417        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1419    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1420        """
1421        Show table contains current prices of given instruments.
1422
1423        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1424                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :return: multilines text in Markdown format as a table contains current prices.
1427        """
1428        infoText = ""
1429
1430        if show or self.pricesFile:
1431            info = [
1432                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1433                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1434                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1435            ]
1436
1437            for item in iList:
1438                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1439                    item["ticker"],
1440                    item["figi"],
1441                    item["type"],
1442                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1443                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1444                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1447                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1448                    ),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1451                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1452                    ),
1453                    item["currency"],
1454                ))
1455
1456            infoText = "".join(info)
1457
1458            if show:
1459                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1460
1461            if self.pricesFile:
1462                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1463                    fH.write(infoText)
1464
1465                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1466
1467                if self.useHTMLReports:
1468                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1469                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1470                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1471
1472                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1473
1474        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1476    def RequestTradingStatus(self) -> dict:
1477        """
1478        Requesting trading status for the instrument defined by `figi` variable.
1479
1480        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1481
1482        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1483
1484        :return: dictionary with trading status attributes. Response example:
1485                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1486                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1487        """
1488        if self._figi is None or not self._figi:
1489            uLogger.error("Variable `figi` must be defined for using this method!")
1490            raise Exception("FIGI required")
1491
1492        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1493
1494        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1495        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1496        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1497
1498        if self.moreDebug:
1499            uLogger.debug("Records about current trading status successfully received")
1500
1501        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1503    def RequestPortfolio(self) -> dict:
1504        """
1505        Requesting actual user's portfolio for current `accountId`.
1506
1507        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1508
1509        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1510
1511        :return: dictionary with user's portfolio.
1512        """
1513        if self.accountId is None or not self.accountId:
1514            uLogger.error("Variable `accountId` must be defined for using this method!")
1515            raise Exception("Account ID required")
1516
1517        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1518
1519        self.body = str({"accountId": self.accountId})
1520        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1521        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1522
1523        if self.moreDebug:
1524            uLogger.debug("Records about user's portfolio successfully received")
1525
1526        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1528    def RequestPositions(self) -> dict:
1529        """
1530        Requesting open positions by currencies and instruments for current `accountId`.
1531
1532        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1533
1534        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1535
1536        :return: dictionary with open positions by instruments.
1537        """
1538        if self.accountId is None or not self.accountId:
1539            uLogger.error("Variable `accountId` must be defined for using this method!")
1540            raise Exception("Account ID required")
1541
1542        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1543
1544        self.body = str({"accountId": self.accountId})
1545        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1546        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1547
1548        if self.moreDebug:
1549            uLogger.debug("Records about current open positions successfully received")
1550
1551        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1553    def RequestPendingOrders(self) -> list:
1554        """
1555        Requesting current actual pending limit orders for current `accountId`.
1556
1557        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1558
1559        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1560
1561        :return: list of dictionaries with pending limit orders.
1562        """
1563        if self.accountId is None or not self.accountId:
1564            uLogger.error("Variable `accountId` must be defined for using this method!")
1565            raise Exception("Account ID required")
1566
1567        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1568
1569        self.body = str({"accountId": self.accountId})
1570        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1571        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1572
1573        if "orders" in rawResponse.keys():
1574            rawOrders = rawResponse["orders"]
1575            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1576
1577        else:
1578            rawOrders = []
1579            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1580
1581        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1583    def RequestStopOrders(self) -> list:
1584        """
1585        Requesting current actual stop orders for current `accountId`.
1586
1587        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1588
1589        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1590
1591        :return: list of dictionaries with stop orders.
1592        """
1593        if self.accountId is None or not self.accountId:
1594            uLogger.error("Variable `accountId` must be defined for using this method!")
1595            raise Exception("Account ID required")
1596
1597        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1598
1599        self.body = str({"accountId": self.accountId})
1600        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1601        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1602
1603        if "stopOrders" in rawResponse.keys():
1604            rawStopOrders = rawResponse["stopOrders"]
1605            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1606
1607        else:
1608            rawStopOrders = []
1609            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1610
1611        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1613    def Overview(self, show: bool = False, details: str = "full") -> dict:
1614        """
1615        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1616        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1617        and `overviewBondsCalendarFile` are defined then also save information to file.
1618
1619        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1620        many requests about the state of the portfolio, and then, based on the received data, a large number
1621        of calculation and statistics are collected.
1622
1623        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1624        :param details: how detailed should the information be?
1625        - `full` — shows full available information about portfolio status (by default),
1626        - `positions` — shows only open positions,
1627        - `orders` — shows only sections of open limits and stop orders.
1628        - `digest` — show a short digest of the portfolio status,
1629        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1630        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1631        :return: dictionary with client's raw portfolio and some statistics.
1632        """
1633        if self.accountId is None or not self.accountId:
1634            uLogger.error("Variable `accountId` must be defined for using this method!")
1635            raise Exception("Account ID required")
1636
1637        view = {
1638            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1639                "headers": {},  # list of dictionaries, response headers without "positions" section
1640                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1641                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1642                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1643                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1644                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1645                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1646                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1647                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1648                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1649            },
1650            "stat": {  # --- some statistics calculated using "raw" sections:
1651                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1652                "availableRUB": 0.,  # available rubles (without other currencies)
1653                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1654                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1655                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1656                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1657                "sharesCostRUB": 0.,  # costs of all shares in RUB
1658                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1659                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1660                "futuresCostRUB": 0.,  # costs of all futures in RUB
1661                "Currencies": [],  # list of dictionaries of all currencies statistics
1662                "Shares": [],  # list of dictionaries of all shares statistics
1663                "Bonds": [],  # list of dictionaries of all bonds statistics
1664                "Etfs": [],  # list of dictionaries of all etfs statistics
1665                "Futures": [],  # list of dictionaries of all futures statistics
1666                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1667                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1668                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1669                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1670                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1671            },
1672            "analytics": {  # --- some analytics of portfolio:
1673                "distrByAssets": {},  # portfolio distribution by assets
1674                "distrByCompanies": {},  # portfolio distribution by companies
1675                "distrBySectors": {},  # portfolio distribution by sectors
1676                "distrByCurrencies": {},  # portfolio distribution by currencies
1677                "distrByCountries": {},  # portfolio distribution by countries
1678                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1679            }
1680        }
1681
1682        details = details.lower()
1683        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1684        if details not in availableDetails:
1685            details = "full"
1686            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1687
1688        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1689
1690        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1691        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1692        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1693        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1694
1695        # save response headers without "positions" section:
1696        for key in portfolioResponse.keys():
1697            if key != "positions":
1698                view["raw"]["headers"][key] = portfolioResponse[key]
1699
1700            else:
1701                continue
1702
1703        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1704        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1705        for item in portfolioResponse["positions"]:
1706            if item["instrumentType"] == "currency":
1707                self._figi = item["figi"]
1708                curr = self.SearchByFIGI(requestPrice=False)
1709
1710                # current price of currency in RUB:
1711                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1712                    "name": curr["name"],
1713                    "currentPrice": NanoToFloat(
1714                        item["currentPrice"]["units"],
1715                        item["currentPrice"]["nano"]
1716                    ),
1717                }
1718
1719                view["raw"]["Currencies"].append(item)
1720
1721            elif item["instrumentType"] == "share":
1722                view["raw"]["Shares"].append(item)
1723
1724            elif item["instrumentType"] == "bond":
1725                view["raw"]["Bonds"].append(item)
1726
1727            elif item["instrumentType"] == "etf":
1728                view["raw"]["Etfs"].append(item)
1729
1730            elif item["instrumentType"] == "futures":
1731                view["raw"]["Futures"].append(item)
1732
1733            else:
1734                continue
1735
1736        # how many volume of currencies (by ISO currency name) are blocked:
1737        for item in view["raw"]["positions"]["blocked"]:
1738            blocked = NanoToFloat(item["units"], item["nano"])
1739            if blocked > 0:
1740                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1741
1742        # how many volume of instruments (by FIGI) are blocked:
1743        for item in view["raw"]["positions"]["securities"]:
1744            blocked = int(item["blocked"])
1745            if blocked > 0:
1746                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1747
1748        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1749
1750        if "rub" in allBlocked.keys():
1751            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1752
1753        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1754        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1755        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1756        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1757        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1758        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1759        view["stat"]["portfolioCostRUB"] = sum([
1760            view["stat"]["allCurrenciesCostRUB"],
1761            view["stat"]["sharesCostRUB"],
1762            view["stat"]["bondsCostRUB"],
1763            view["stat"]["etfsCostRUB"],
1764            view["stat"]["futuresCostRUB"],
1765        ])
1766
1767        # --- calculating some portfolio statistics:
1768        byComp = {}  # distribution by companies
1769        bySect = {}  # distribution by sectors
1770        byCurr = {}  # distribution by currencies (include RUB)
1771        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1772        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1773
1774        for item in portfolioResponse["positions"]:
1775            self._figi = item["figi"]
1776            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1777
1778            if instrument:
1779                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1780                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1781
1782                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1783                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1784
1785                else:
1786                    blocked = 0
1787
1788                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1789                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1790                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1791                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1792                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1793                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1794                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1795                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1796                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1797                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1798                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1799                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1800
1801                statData = {
1802                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1803                    "ticker": instrument["ticker"],  # ticker by FIGI
1804                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1805                    "volume": volume,  # available volume of instrument
1806                    "lots": lots,  # volume in lots of instrument
1807                    "direction": direction,  # direction of an instrument's position: short or long
1808                    "blocked": blocked,  # blocked volume of currency or instrument
1809                    "currentPrice": curPrice,  # current instrument's price in basic asset
1810                    "average": average,  # current average position price
1811                    "cost": cost,  # current cost of all volume of instrument in basic asset
1812                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1813                    "costRUB": costRUB,  # cost of instrument in ruble
1814                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1815                    "profit": profit,  # expected profit at current moment
1816                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1817                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1818                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1819                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1820                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1821                    "step": instrument["step"],  # minimum price increment
1822                }
1823
1824                # adding distribution by unique countries:
1825                if statData["country"] not in byCountry.keys():
1826                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1827
1828                else:
1829                    byCountry[statData["country"]]["cost"] += costRUB
1830                    byCountry[statData["country"]]["percent"] += percentCostRUB
1831
1832                if item["instrumentType"] != "currency":
1833                    # adding distribution by unique companies:
1834                    if statData["name"]:
1835                        if statData["name"] not in byComp.keys():
1836                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1837
1838                        else:
1839                            byComp[statData["name"]]["cost"] += costRUB
1840                            byComp[statData["name"]]["percent"] += percentCostRUB
1841
1842                    # adding distribution by unique sectors:
1843                    if statData["sector"] not in bySect.keys():
1844                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1845
1846                    else:
1847                        bySect[statData["sector"]]["cost"] += costRUB
1848                        bySect[statData["sector"]]["percent"] += percentCostRUB
1849
1850                # adding distribution by unique currencies:
1851                if currency not in byCurr.keys():
1852                    byCurr[currency] = {
1853                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1854                        "cost": costRUB,
1855                        "percent": percentCostRUB
1856                    }
1857
1858                else:
1859                    byCurr[currency]["cost"] += costRUB
1860                    byCurr[currency]["percent"] += percentCostRUB
1861
1862                # saving statistics for every instrument:
1863                if item["instrumentType"] == "currency":
1864                    view["stat"]["Currencies"].append(statData)
1865
1866                    # update dict with free funds for trading (total - blocked) by currencies
1867                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1868                    view["stat"]["funds"][currency] = {
1869                        "total": volume,
1870                        "totalCostRUB": costRUB,  # total volume cost in rubles
1871                        "free": volume - blocked,
1872                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1873                    }
1874
1875                elif item["instrumentType"] == "share":
1876                    view["stat"]["Shares"].append(statData)
1877
1878                elif item["instrumentType"] == "bond":
1879                    view["stat"]["Bonds"].append(statData)
1880
1881                elif item["instrumentType"] == "etf":
1882                    view["stat"]["Etfs"].append(statData)
1883
1884                elif item["instrumentType"] == "Futures":
1885                    view["stat"]["Futures"].append(statData)
1886
1887                else:
1888                    continue
1889
1890        # total changes in Russian Ruble:
1891        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1892        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1893        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1894        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1895        view["stat"]["funds"]["rub"] = {
1896            "total": view["stat"]["availableRUB"],
1897            "totalCostRUB": view["stat"]["availableRUB"],
1898            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1899            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1900        }
1901
1902        # --- pending limit orders sector data:
1903        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1904        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1905
1906        for item in view["raw"]["orders"]:
1907            self._figi = item["figi"]
1908
1909            if item["figi"] not in uniquePendingOrdersFIGIs:
1910                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1911
1912                uniquePendingOrdersFIGIs.append(item["figi"])
1913                uniquePendingOrders[item["figi"]] = instrument
1914
1915            else:
1916                instrument = uniquePendingOrders[item["figi"]]
1917
1918            if instrument:
1919                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1920                orderType = TKS_ORDER_TYPES[item["orderType"]]
1921                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1922                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1923
1924                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1925                if item["direction"] == "ORDER_DIRECTION_BUY":
1926                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1927
1928                else:
1929                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1930
1931                # requested price for order execution:
1932                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1933
1934                # necessary changes in percent to reach target from current price:
1935                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1936
1937                view["stat"]["orders"].append({
1938                    "orderID": item["orderId"],  # orderId number parameter of current order
1939                    "figi": item["figi"],  # FIGI identification
1940                    "ticker": instrument["ticker"],  # ticker name by FIGI
1941                    "lotsRequested": item["lotsRequested"],  # requested lots value
1942                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1943                    "currentPrice": lastPrice,  # current instrument's price for defined action
1944                    "targetPrice": target,  # requested price for order execution in base currency
1945                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1946                    "percentChanges": changes,  # changes in percent to target from current price
1947                    "currency": item["currency"],  # instrument's currency name
1948                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1949                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1950                    "status": orderState,  # order status from TKS_ORDER_STATES
1951                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1952                })
1953
1954        # --- stop orders sector data:
1955        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1956        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1957
1958        for item in view["raw"]["stopOrders"]:
1959            self._figi = item["figi"]
1960
1961            if item["figi"] not in uniqueStopOrdersFIGIs:
1962                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1963
1964                uniqueStopOrdersFIGIs.append(item["figi"])
1965                uniqueStopOrders[item["figi"]] = instrument
1966
1967            else:
1968                instrument = uniqueStopOrders[item["figi"]]
1969
1970            if instrument:
1971                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1972                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1973                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1974
1975                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1976                if "expirationTime" in item.keys():
1977                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1978                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1979
1980                else:
1981                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1982                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1983
1984                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1985                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1986                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1987
1988                else:
1989                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1990
1991                # requested price when stop-order executed:
1992                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1993
1994                # price for limit-order, set up when stop-order executed:
1995                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1996
1997                # necessary changes in percent to reach target from current price:
1998                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1999
2000                view["stat"]["stopOrders"].append({
2001                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2002                    "figi": item["figi"],  # FIGI identification
2003                    "ticker": instrument["ticker"],  # ticker name by FIGI
2004                    "lotsRequested": item["lotsRequested"],  # requested lots value
2005                    "currentPrice": lastPrice,  # current instrument's price for defined action
2006                    "targetPrice": target,  # requested price for stop-order execution in base currency
2007                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2008                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2009                    "percentChanges": changes,  # changes in percent to target from current price
2010                    "currency": item["currency"],  # instrument's currency name
2011                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2012                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2013                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2014                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2015                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2016                })
2017
2018        # --- calculating data for analytics section:
2019        # portfolio distribution by assets:
2020        view["analytics"]["distrByAssets"] = {
2021            "Ruble": {
2022                "uniques": 1,
2023                "cost": view["stat"]["availableRUB"],
2024                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2025            },
2026            "Currencies": {
2027                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2028                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2029                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2030            },
2031            "Shares": {
2032                "uniques": len(view["stat"]["Shares"]),
2033                "cost": view["stat"]["sharesCostRUB"],
2034                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2035            },
2036            "Bonds": {
2037                "uniques": len(view["stat"]["Bonds"]),
2038                "cost": view["stat"]["bondsCostRUB"],
2039                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2040            },
2041            "Etfs": {
2042                "uniques": len(view["stat"]["Etfs"]),
2043                "cost": view["stat"]["etfsCostRUB"],
2044                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2045            },
2046            "Futures": {
2047                "uniques": len(view["stat"]["Futures"]),
2048                "cost": view["stat"]["futuresCostRUB"],
2049                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2050            },
2051        }
2052
2053        # portfolio distribution by companies:
2054        view["analytics"]["distrByCompanies"]["All money cash"] = {
2055            "ticker": "",
2056            "cost": view["stat"]["allCurrenciesCostRUB"],
2057            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058        }
2059        view["analytics"]["distrByCompanies"].update(byComp)
2060
2061        # portfolio distribution by sectors:
2062        view["analytics"]["distrBySectors"]["All money cash"] = {
2063            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2064            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2065        }
2066        view["analytics"]["distrBySectors"].update(bySect)
2067
2068        # portfolio distribution by currencies:
2069        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2070            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2071
2072            if self.moreDebug:
2073                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2074
2075        view["analytics"]["distrByCurrencies"].update(byCurr)
2076        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2077        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2078
2079        # portfolio distribution by countries:
2080        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2081            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2082
2083            if self.moreDebug:
2084                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2085
2086        view["analytics"]["distrByCountries"].update(byCountry)
2087        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2088        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2089
2090        # --- Prepare text statistics overview in human-readable:
2091        if show:
2092            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2093
2094            # Whatever the value `details`, header not changes:
2095            info = [
2096                "# Client's portfolio\n\n",
2097                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2098                "* **Account ID:** [{}]\n".format(self.accountId),
2099            ]
2100
2101            if details in ["full", "positions", "digest"]:
2102                info.extend([
2103                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2104                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2105                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2106                        view["stat"]["totalChangesRUB"],
2107                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2108                        view["stat"]["totalChangesPercentRUB"],
2109                    ),
2110                ])
2111
2112            if details in ["full", "positions"]:
2113                info.extend([
2114                    "## Open positions\n\n",
2115                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2116                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2117                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2118                        "{:.2f} ({:.2f}) rub".format(
2119                            view["stat"]["availableRUB"],
2120                            view["stat"]["blockedRUB"],
2121                        )
2122                    )
2123                ])
2124
2125                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2126                    return [
2127                        "|                             |                                 |          |              |              |                     |                              |\n",
2128                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2129                            noTradeStr if noTradeStr else typeStr,
2130                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2131                        ),
2132                    ]
2133
2134                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2135                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2136                        "{} [{}]".format(data["ticker"], data["figi"]),
2137                        "{:.2f} ({:.2f}) {}".format(
2138                            data["volume"],
2139                            data["blocked"],
2140                            data["currency"],
2141                        ) if isCurr else "{:.0f} ({:.0f})".format(
2142                            data["volume"],
2143                            data["blocked"],
2144                        ),
2145                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2146                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2147                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2148                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2149                        "{}{:.2f} {} ({}{:.2f}%)".format(
2150                            "+" if data["profit"] > 0 else "",
2151                            data["profit"], data["baseCurrencyName"],
2152                            "+" if data["percentProfit"] > 0 else "",
2153                            data["percentProfit"],
2154                        ),
2155                    )
2156
2157                # --- Show currencies section:
2158                if view["stat"]["Currencies"]:
2159                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2160                    for item in view["stat"]["Currencies"]:
2161                        info.append(_InfoStr(item, isCurr=True))
2162
2163                else:
2164                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2165
2166                # --- Show shares section:
2167                if view["stat"]["Shares"]:
2168                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2169
2170                    for item in view["stat"]["Shares"]:
2171                        info.append(_InfoStr(item))
2172
2173                else:
2174                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2175
2176                # --- Show bonds section:
2177                if view["stat"]["Bonds"]:
2178                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2179
2180                    for item in view["stat"]["Bonds"]:
2181                        info.append(_InfoStr(item))
2182
2183                else:
2184                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2185
2186                # --- Show etfs section:
2187                if view["stat"]["Etfs"]:
2188                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2189
2190                    for item in view["stat"]["Etfs"]:
2191                        info.append(_InfoStr(item))
2192
2193                else:
2194                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2195
2196                # --- Show futures section:
2197                if view["stat"]["Futures"]:
2198                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2199
2200                    for item in view["stat"]["Futures"]:
2201                        info.append(_InfoStr(item))
2202
2203                else:
2204                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2205
2206            if details in ["full", "orders"]:
2207                # --- Show pending limit orders section:
2208                if view["stat"]["orders"]:
2209                    info.extend([
2210                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2211                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2212                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2213                    ])
2214
2215                    for item in view["stat"]["orders"]:
2216                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2217                            "{} [{}]".format(item["ticker"], item["figi"]),
2218                            item["orderID"],
2219                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2220                            "{} {} ({}{:.2f}%)".format(
2221                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2222                                item["baseCurrencyName"],
2223                                "+" if item["percentChanges"] > 0 else "",
2224                                float(item["percentChanges"]),
2225                            ),
2226                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2227                            item["action"],
2228                            item["type"],
2229                            item["date"],
2230                        ))
2231
2232                else:
2233                    info.append("\n## Total pending limit-orders: [0]\n")
2234
2235                # --- Show stop orders section:
2236                if view["stat"]["stopOrders"]:
2237                    info.extend([
2238                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2239                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2240                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2241                    ])
2242
2243                    for item in view["stat"]["stopOrders"]:
2244                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2245                            "{} [{}]".format(item["ticker"], item["figi"]),
2246                            item["orderID"],
2247                            item["lotsRequested"],
2248                            "{} {} ({}{:.2f}%)".format(
2249                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2250                                item["baseCurrencyName"],
2251                                "+" if item["percentChanges"] > 0 else "",
2252                                float(item["percentChanges"]),
2253                            ),
2254                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2255                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2256                            item["action"],
2257                            item["type"],
2258                            item["expType"],
2259                            item["createDate"],
2260                            item["expDate"],
2261                        ))
2262
2263                else:
2264                    info.append("\n## Total stop-orders: [0]\n")
2265
2266            if details in ["full", "analytics"]:
2267                # -- Show analytics section:
2268                if view["stat"]["portfolioCostRUB"] > 0:
2269                    info.extend([
2270                        "\n# Analytics\n\n"
2271                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2272                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2273                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2274                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2275                            view["stat"]["totalChangesRUB"],
2276                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2277                            view["stat"]["totalChangesPercentRUB"],
2278                        ),
2279                        "\n## Portfolio distribution by assets\n"
2280                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2281                        "|------------------------------------|---------|---------|--------------------|\n",
2282                    ])
2283
2284                    for key in view["analytics"]["distrByAssets"].keys():
2285                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2286                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2287                                key,
2288                                view["analytics"]["distrByAssets"][key]["uniques"],
2289                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2290                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2291                            ))
2292
2293                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2294
2295                    info.extend([
2296                        "\n## Portfolio distribution by companies\n"
2297                        "\n| Company                                      | Percent | Current cost       |\n",
2298                        aSepLine,
2299                    ])
2300
2301                    for company in view["analytics"]["distrByCompanies"].keys():
2302                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2303                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2304                                "{}{}".format(
2305                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2306                                    company,
2307                                ),
2308                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2309                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2310                            ))
2311
2312                    info.extend([
2313                        "\n## Portfolio distribution by sectors\n"
2314                        "\n| Sector                                       | Percent | Current cost       |\n",
2315                        aSepLine,
2316                    ])
2317
2318                    for sector in view["analytics"]["distrBySectors"].keys():
2319                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2320                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2321                                sector,
2322                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2323                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2324                            ))
2325
2326                    info.extend([
2327                        "\n## Portfolio distribution by currencies\n"
2328                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2329                        aSepLine,
2330                    ])
2331
2332                    for curr in view["analytics"]["distrByCurrencies"].keys():
2333                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2334                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2335                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2336                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2337                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2338                            ))
2339
2340                    info.extend([
2341                        "\n## Portfolio distribution by countries\n"
2342                        "\n| Assets by country                            | Percent | Current cost       |\n",
2343                        aSepLine,
2344                    ])
2345
2346                    for country in view["analytics"]["distrByCountries"].keys():
2347                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2348                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2349                                country,
2350                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2351                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2352                            ))
2353
2354            if details in ["full", "calendar"]:
2355                # -- Show bonds payment calendar section:
2356                if view["stat"]["Bonds"]:
2357                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2358                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2359                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2360
2361                else:
2362                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2363
2364            infoText = "".join(info)
2365
2366            uLogger.info(infoText)
2367
2368            if details == "full" and self.overviewFile:
2369                filename = self.overviewFile
2370
2371            elif details == "digest" and self.overviewDigestFile:
2372                filename = self.overviewDigestFile
2373
2374            elif details == "positions" and self.overviewPositionsFile:
2375                filename = self.overviewPositionsFile
2376
2377            elif details == "orders" and self.overviewOrdersFile:
2378                filename = self.overviewOrdersFile
2379
2380            elif details == "analytics" and self.overviewAnalyticsFile:
2381                filename = self.overviewAnalyticsFile
2382
2383            elif details == "calendar" and self.overviewBondsCalendarFile:
2384                filename = self.overviewBondsCalendarFile
2385
2386            else:
2387                filename = ""
2388
2389            if filename:
2390                with open(filename, "w", encoding="UTF-8") as fH:
2391                    fH.write(infoText)
2392
2393                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2394
2395                if self.useHTMLReports:
2396                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2397                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2398                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText))
2399
2400                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2401
2402        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2404    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2405        """
2406        Returns history operations between two given dates for current `accountId`.
2407        If `reportFile` string is not empty then also save human-readable report.
2408        Shows some statistical data of closed positions.
2409
2410        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2411        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2412        :param show: if `True` then also prints all records to the console.
2413        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2414        :return: original list of dictionaries with history of deals records from API ("operations" key):
2415                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2416                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2417        """
2418        if self.accountId is None or not self.accountId:
2419            uLogger.error("Variable `accountId` must be defined for using this method!")
2420            raise Exception("Account ID required")
2421
2422        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2423
2424        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2425
2426        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2427        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2428        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2429        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2430        customStat = {}  # custom statistics in additional to responseJSON
2431
2432        # --- output report in human-readable format:
2433        if show or self.reportFile:
2434            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2435            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2436            nextDay = ""
2437
2438            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2439
2440            if len(ops) > 0:
2441                customStat = {
2442                    "opsCount": 0,  # total operations count
2443                    "buyCount": 0,  # buy operations
2444                    "sellCount": 0,  # sell operations
2445                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2446                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2447                    "payIn": {"rub": 0.},  # Deposit brokerage account
2448                    "payOut": {"rub": 0.},  # Withdrawals
2449                    "divs": {"rub": 0.},  # Dividends income
2450                    "coupons": {"rub": 0.},  # Coupon's income
2451                    "brokerCom": {"rub": 0.},  # Service commissions
2452                    "serviceCom": {"rub": 0.},  # Service commissions
2453                    "marginCom": {"rub": 0.},  # Margin commissions
2454                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2455                }
2456
2457                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2458                for item in ops:
2459                    if item["state"] == "OPERATION_STATE_EXECUTED":
2460                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2461
2462                        # count buy operations:
2463                        if "_BUY" in item["operationType"]:
2464                            customStat["buyCount"] += 1
2465
2466                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2467                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2468
2469                            else:
2470                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2471
2472                        # count sell operations:
2473                        elif "_SELL" in item["operationType"]:
2474                            customStat["sellCount"] += 1
2475
2476                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2477                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2481
2482                        # count incoming operations:
2483                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2484                            if item["payment"]["currency"] in customStat["payIn"].keys():
2485                                customStat["payIn"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["payIn"][item["payment"]["currency"]] = payment
2489
2490                        # count withdrawals operations:
2491                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2492                            if item["payment"]["currency"] in customStat["payOut"].keys():
2493                                customStat["payOut"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["payOut"][item["payment"]["currency"]] = payment
2497
2498                        # count dividends income:
2499                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2500                            if item["payment"]["currency"] in customStat["divs"].keys():
2501                                customStat["divs"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["divs"][item["payment"]["currency"]] = payment
2505
2506                        # count coupon's income:
2507                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2508                            if item["payment"]["currency"] in customStat["coupons"].keys():
2509                                customStat["coupons"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["coupons"][item["payment"]["currency"]] = payment
2513
2514                        # count broker commissions:
2515                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2516                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2517                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2521
2522                        # count service commissions:
2523                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2524                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2525                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2529
2530                        # count margin commissions:
2531                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2532                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2533                                customStat["marginCom"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["marginCom"][item["payment"]["currency"]] = payment
2537
2538                        # count withholding taxes:
2539                        elif "_TAX" in item["operationType"]:
2540                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2541                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2545
2546                        else:
2547                            continue
2548
2549                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2550
2551                # --- view "Actions" lines:
2552                info.extend([
2553                    "| Report sections            |                               |                              |                      |                        |\n",
2554                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2555                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2556                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2557                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2558                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2559                    ),
2560                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2561                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2562                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2563                    ),
2564                ])
2565
2566                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2567                for key in opsKeys:
2568                    if key == "rub":
2569                        continue
2570
2571                    info.extend([
2572                        "|                            |                               | {:<28} |                      |                        |\n".format(
2573                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2574                        ),
2575                        "|                            |                               | {:<28} |                      |                        |\n".format(
2576                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2577                        ),
2578                    ])
2579
2580                info.append(splitLine1)
2581
2582                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2583                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2584                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2585                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2586                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2587                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2588                    )
2589
2590                # --- view "Payments" lines:
2591                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2592                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2593
2594                for key in paymentsKeys:
2595                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2596
2597                info.append(splitLine1)
2598
2599                # --- view "Commissions and taxes" lines:
2600                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2601                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2602
2603                for key in comKeys:
2604                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2605
2606                info.extend([
2607                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2608                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2609                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2610                ])
2611
2612            else:
2613                info.append("Broker returned no operations during this period\n")
2614
2615            # --- view "Operations" section:
2616            for item in ops:
2617                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2618                    continue
2619
2620                else:
2621                    self._figi = item["figi"] if item["figi"] else ""
2622                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2623                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2624
2625                    # group of deals during one day:
2626                    if nextDay and item["date"].split("T")[0] != nextDay:
2627                        info.append(splitLine2)
2628                        nextDay = ""
2629
2630                    else:
2631                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2632
2633                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2634                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2635                        self._figi if self._figi else "—",
2636                        instrument["ticker"] if instrument else "—",
2637                        instrument["type"] if instrument else "—",
2638                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2639                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2640                        TKS_OPERATION_STATES[item["state"]],
2641                        TKS_OPERATION_TYPES[item["operationType"]],
2642                    ))
2643
2644            infoText = "".join(info)
2645
2646            if show:
2647                if self.moreDebug:
2648                    uLogger.debug("Records about history of a client's operations successfully received")
2649
2650                uLogger.info(infoText)
2651
2652            if self.reportFile:
2653                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2654                    fH.write(infoText)
2655
2656                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2657
2658                if self.useHTMLReports:
2659                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2660                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2661                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2662
2663                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2664
2665        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2667    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2668        """
2669        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2670
2671        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2672        Warning! Broker server used ISO UTC time by default.
2673
2674        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2675        Also, `historyFile` used to update history with `onlyMissing` parameter.
2676
2677        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2678
2679        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2680        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2681        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2682                         `"hour"`, `"day"`. Default: `"hour"`.
2683        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2684                            False by default. Warning! History appends only from last candle to current time
2685                            with always update last candle!
2686        :param csvSep: separator if csv-file is used, `,` by default.
2687        :param show: if `True` then also prints Pandas DataFrame to the console.
2688        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2689                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2690        """
2691        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2692        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2693        history = None  # empty pandas object for history
2694
2695        if interval not in TKS_CANDLE_INTERVALS.keys():
2696            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2697            raise Exception("Incorrect value")
2698
2699        if not (self._ticker or self._figi):
2700            uLogger.error("Ticker or FIGI must be defined!")
2701            raise Exception("Ticker or FIGI required")
2702
2703        if self._ticker and not self._figi:
2704            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2705            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2706
2707        if self._figi and not self._ticker:
2708            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2709            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2710
2711        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2712        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2713        if interval.lower() != "day":
2714            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2715
2716        delta = dtEnd - dtStart  # current UTC time minus last time in file
2717        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2718
2719        # calculate history length in candles:
2720        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2721        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2722            length += 1  # to avoid fraction time
2723
2724        # calculate data blocks count:
2725        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2726
2727        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2728        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2729        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2730        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2731        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2732
2733        tempOld = None  # pandas object for old history, if --only-missing key present
2734        lastTime = None  # datetime object of last old candle in file
2735
2736        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2737            uLogger.debug("--only-missing key present, add only last missing candles...")
2738            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2739
2740            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2741
2742            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2743            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2744            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2745            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2746
2747            # get last datetime object from last string in file or minus 1 delta if file is empty:
2748            if len(tempOld) > 0:
2749                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2750
2751            else:
2752                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2753
2754            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2755
2756        responseJSONs = []  # raw history blocks of data
2757
2758        blockEnd = dtEnd
2759        for item in range(blocks):
2760            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2761            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2762
2763            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2764                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2765            ))
2766
2767            if blockStart == blockEnd:
2768                uLogger.debug("Skipped this zero-length block...")
2769
2770            else:
2771                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2772                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2773                self.body = str({
2774                    "figi": self._figi,
2775                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2776                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2777                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2778                })
2779                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2780
2781                if "code" in responseJSON.keys():
2782                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2783
2784                else:
2785                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2786                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2787
2788                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2789
2790            blockEnd = blockStart
2791
2792        printCount = len(responseJSONs)  # candles to show in console
2793        if responseJSONs:
2794            tempHistory = pd.DataFrame(
2795                data={
2796                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2797                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2798                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2799                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2800                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2801                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2802                    "volume": [int(item["volume"]) for item in responseJSONs],
2803                },
2804                index=range(len(responseJSONs)),
2805                columns=["date", "time", "open", "high", "low", "close", "volume"],
2806            )
2807            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2808            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2809
2810            # append only newest candles to old history if --only-missing key present:
2811            if onlyMissing and tempOld is not None and lastTime is not None:
2812                index = 0  # find start index in tempHistory data:
2813
2814                for i, item in tempHistory.iterrows():
2815                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2816
2817                    if curTime == lastTime:
2818                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2819                        index = i
2820                        printCount = index + 1
2821                        break
2822
2823                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2824
2825            else:
2826                history = tempHistory  # if no `--only-missing` key then load full data from server
2827
2828            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2829
2830        if history is not None and not history.empty:
2831            if show:
2832                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2833                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2834                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2835                ))
2836
2837        else:
2838            uLogger.warning("Received an empty candles history!")
2839
2840        if self.historyFile is not None:
2841            if history is not None and not history.empty:
2842                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2843                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2844
2845            else:
2846                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2847
2848        else:
2849            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2850
2851        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2853    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2854        """
2855        Load candles history from csv-file and return Pandas DataFrame object.
2856
2857        See also: `History()` and `ShowHistoryChart()` methods.
2858
2859        :param filePath: path to csv-file to open.
2860        """
2861        loadedHistory = None  # init candles data object
2862
2863        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2864
2865        if os.path.exists(filePath):
2866            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2867
2868            tfStr = self.priceModel.FormattedDelta(
2869                self.priceModel.timeframe,
2870                "{days} days {hours}h {minutes}m {seconds}s",
2871            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2872                self.priceModel.timeframe,
2873                "{hours}h {minutes}m {seconds}s",
2874            )
2875
2876            if loadedHistory is not None and not loadedHistory.empty:
2877                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2878                    len(loadedHistory),
2879                    tfStr,
2880                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2881                )
2882
2883            else:
2884                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2885
2886        else:
2887            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2888
2889        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2891    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2892        """
2893        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2894
2895        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2896        Default: `index.html` (both for interact and non-interact candlesticks chart).
2897
2898        See also: `History()` and `LoadHistory()` methods.
2899
2900        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2901        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2902                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2903                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2904                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2905        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2906                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2907        """
2908        if isinstance(candles, str):
2909            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2910            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2911
2912        elif isinstance(candles, pd.DataFrame):
2913            self.priceModel.prices = candles  # set candles chain from variable
2914            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2915
2916            if "datetime" not in candles.columns:
2917                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2918
2919        else:
2920            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2921            raise Exception("Incorrect value")
2922
2923        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2924
2925        if interact:
2926            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2927
2928            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2929
2930        else:
2931            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2932
2933            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2934
2935        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2937    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2938        """
2939        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2940        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2941
2942        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2943
2944        :param operation: string "Buy" or "Sell".
2945        :param lots: volume, integer count of lots >= 1.
2946        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2947        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2948        :param expDate: string "Undefined" by default or local date in future,
2949                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2950        :return: JSON with response from broker server.
2951        """
2952        if self.accountId is None or not self.accountId:
2953            uLogger.error("Variable `accountId` must be defined for using this method!")
2954            raise Exception("Account ID required")
2955
2956        if operation is None or not operation or operation not in ("Buy", "Sell"):
2957            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2958            raise Exception("Incorrect value")
2959
2960        if lots is None or lots < 1:
2961            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2962            lots = 1
2963
2964        if tp is None or tp < 0:
2965            tp = 0
2966
2967        if sl is None or sl < 0:
2968            sl = 0
2969
2970        if expDate is None or not expDate:
2971            expDate = "Undefined"
2972
2973        if not (self._ticker or self._figi):
2974            uLogger.error("Ticker or FIGI must be defined!")
2975            raise Exception("Ticker or FIGI required")
2976
2977        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2978        self._ticker = instrument["ticker"]
2979        self._figi = instrument["figi"]
2980
2981        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2982
2983        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2984        self.body = str({
2985            "figi": self._figi,
2986            "quantity": str(lots),
2987            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2988            "accountId": str(self.accountId),
2989            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2990        })
2991        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2992
2993        if "orderId" in response.keys():
2994            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2995                operation, response["orderId"],
2996                self._ticker, self._figi, lots,
2997                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2998                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2999                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3000            ))
3001
3002            if tp > 0:
3003                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3004
3005            if sl > 0:
3006                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3007
3008        else:
3009            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3010
3011        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3013    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3014        """
3015        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3016        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3017
3018        See also: `Order()` and `Trade()` docstrings.
3019
3020        :param lots: volume, integer count of lots >= 1.
3021        :param tp: float > 0, take profit price of stop-order.
3022        :param sl: float > 0, stop loss price of stop-order.
3023        :param expDate: it's a local date in future.
3024                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3025        :return: JSON with response from broker server.
3026        """
3027        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3029    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3030        """
3031        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3032        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3033
3034        See also: `Order()` and `Trade()` docstrings.
3035
3036        :param lots: volume, integer count of lots >= 1.
3037        :param tp: float > 0, take profit price of stop-order.
3038        :param sl: float > 0, stop loss price of stop-order.
3039        :param expDate: it's a local date in the future.
3040                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3041        :return: JSON with response from broker server.
3042        """
3043        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3045    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3046        """
3047        Close position of given instruments.
3048
3049        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3050        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3051                         This avoids unnecessary downloading data from the server.
3052        """
3053        if instruments is None or not instruments:
3054            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3055            raise Exception("Ticker or FIGI required")
3056
3057        if isinstance(instruments, str):
3058            instruments = [instruments]
3059
3060        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3061        if uniqueInstruments:
3062            if portfolio is None or not portfolio:
3063                portfolio = self.Overview(show=False)
3064
3065            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3066            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3067
3068            for self._figi in uniqueInstruments:
3069                if self._figi not in allOpened:
3070                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3071                    continue
3072
3073                # search open trade info about instrument by ticker:
3074                instrument = {}
3075                for iType in TKS_INSTRUMENTS:
3076                    if instrument:
3077                        break
3078
3079                    for item in portfolio["stat"][iType]:
3080                        if item["figi"] == self._figi:
3081                            instrument = item
3082                            break
3083
3084                if instrument:
3085                    self._ticker = instrument["ticker"]
3086                    self._figi = instrument["figi"]
3087
3088                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3089                        self._ticker,
3090                        self._figi,
3091                        int(instrument["volume"]),
3092                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3093                    ))
3094
3095                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3096
3097                    if tradeLots > 0:
3098                        if instrument["blocked"] > 0:
3099                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3100                                instrument["blocked"],
3101                                self._ticker,
3102                                tradeLots,
3103                            ))
3104
3105                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3106                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3107
3108                    else:
3109                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3111    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3112        """
3113        Close all positions of given instruments with defined type.
3114
3115        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3116        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3117                         This avoids unnecessary downloading data from the server.
3118        """
3119        if iType not in TKS_INSTRUMENTS:
3120            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3121
3122        else:
3123            if portfolio is None or not portfolio:
3124                portfolio = self.Overview(show=False)
3125
3126            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3127            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3128
3129            if tickers and portfolio:
3130                self.CloseTrades(tickers, portfolio)
3131
3132            else:
3133                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3135    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3136        """
3137        Universal method to create market or limit orders with all available parameters for current `accountId`.
3138        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3139
3140        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3141        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3142
3143        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3144        then broker immediately open market order as you can do simple --buy or --sell operations!
3145
3146        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3147        When current price will go up or down to target price value then broker opens a limit order.
3148        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3149
3150        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3151
3152        :param operation: string "Buy" or "Sell".
3153        :param orderType: string "Limit" or "Stop".
3154        :param lots: volume, integer count of lots >= 1.
3155        :param targetPrice: target price > 0. This is open trade price for limit order.
3156        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3157                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3158        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3159                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3160                         Stop loss order always executed by market price.
3161        :param expDate: string "Undefined" by default or local date in future.
3162                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3163                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3164                        A limit order has no expiration date, it lasts until the end of the trading day.
3165        :return: JSON with response from broker server.
3166        """
3167        if self.accountId is None or not self.accountId:
3168            uLogger.error("Variable `accountId` must be defined for using this method!")
3169            raise Exception("Account ID required")
3170
3171        if operation is None or not operation or operation not in ("Buy", "Sell"):
3172            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3173            raise Exception("Incorrect value")
3174
3175        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3176            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3177            raise Exception("Incorrect value")
3178
3179        if lots is None or lots < 1:
3180            uLogger.error("You must define trade volume > 0: integer count of lots!")
3181            raise Exception("Incorrect value")
3182
3183        if targetPrice is None or targetPrice <= 0:
3184            uLogger.error("Target price for limit-order must be greater than 0!")
3185            raise Exception("Incorrect value")
3186
3187        if limitPrice is None or limitPrice <= 0:
3188            limitPrice = targetPrice
3189
3190        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3191            stopType = "Limit"
3192
3193        if expDate is None or not expDate:
3194            expDate = "Undefined"
3195
3196        if not (self._ticker or self._figi):
3197            uLogger.error("Tocker or FIGI must be defined!")
3198            raise Exception("Ticker or FIGI required")
3199
3200        response = {}
3201        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3202        self._ticker = instrument["ticker"]
3203        self._figi = instrument["figi"]
3204
3205        if orderType == "Limit":
3206            uLogger.debug(
3207                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3208                    self._ticker, self._figi,
3209                    operation, lots, targetPrice, instrument["currency"],
3210                ))
3211
3212            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3213            self.body = str({
3214                "figi": self._figi,
3215                "quantity": str(lots),
3216                "price": FloatToNano(targetPrice),
3217                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3218                "accountId": str(self.accountId),
3219                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3220            })
3221            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3222
3223            if "orderId" in response.keys():
3224                uLogger.info(
3225                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3226                        response["orderId"], self._ticker, self._figi, operation, lots,
3227                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3228                    ))
3229
3230                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3231                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3232                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3233                            targetPrice, instrument["currency"],
3234                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3235                        ))
3236
3237                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3238                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3239                            targetPrice, instrument["currency"],
3240                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3241                        ))
3242
3243            else:
3244                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3245
3246        if orderType == "Stop":
3247            uLogger.debug(
3248                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3249                    self._ticker, self._figi,
3250                    operation, lots,
3251                    targetPrice, instrument["currency"],
3252                    limitPrice, instrument["currency"],
3253                    stopType, expDate,
3254                ))
3255
3256            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3257            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3258            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3259
3260            body = {
3261                "figi": self._figi,
3262                "quantity": str(lots),
3263                "price": FloatToNano(limitPrice),
3264                "stopPrice": FloatToNano(targetPrice),
3265                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3266                "accountId": str(self.accountId),
3267                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3268                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3269            }
3270
3271            if expDateUTC:
3272                body["expireDate"] = expDateUTC
3273
3274            self.body = str(body)
3275            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3276
3277            if "stopOrderId" in response.keys():
3278                uLogger.info(
3279                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3280                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3281                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3282                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3283                        TKS_STOP_ORDER_TYPES[stopOrderType],
3284                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3285                    ))
3286
3287                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3288                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3289                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3290                            targetPrice, instrument["currency"],
3291                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3292                        ))
3293
3294                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3295                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3296                            targetPrice, instrument["currency"],
3297                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3298                        ))
3299
3300            else:
3301                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3302
3303        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3305    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3306        """
3307        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3308        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3309        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3310        See also: `Order()` docstring.
3311
3312        :param lots: volume, integer count of lots >= 1.
3313        :param targetPrice: target price > 0. This is open trade price for limit order.
3314        :return: JSON with response from broker server.
3315        """
3316        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3318    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3319        """
3320        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3321        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3322        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3323        target price value then broker opens a limit order. See also: `Order()` docstring.
3324
3325        :param lots: volume, integer count of lots >= 1.
3326        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3327        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3328                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3329        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3330                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3331        :param expDate: string "Undefined" by default or local date in future.
3332                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3333                        This date is converting to UTC format for server.
3334        :return: JSON with response from broker server.
3335        """
3336        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3338    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3339        """
3340        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3341        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3342        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3343        See also: `Order()` docstring.
3344
3345        :param lots: volume, integer count of lots >= 1.
3346        :param targetPrice: target price > 0. This is open trade price for limit order.
3347        :return: JSON with response from broker server.
3348        """
3349        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3351    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3352        """
3353        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3354        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3355        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3356        target price value then broker opens a limit order. See also: `Order()` docstring.
3357
3358        :param lots: volume, integer count of lots >= 1.
3359        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3360        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3361                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3362        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3363                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3364        :param expDate: string "Undefined" by default or local date in future.
3365                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3366                        This date is converting to UTC format for server.
3367        :return: JSON with response from broker server.
3368        """
3369        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3371    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3372        """
3373        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3374
3375        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3376        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3377                             This avoids unnecessary downloading data from the server.
3378        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3379        """
3380        if self.accountId is None or not self.accountId:
3381            uLogger.error("Variable `accountId` must be defined for using this method!")
3382            raise Exception("Account ID required")
3383
3384        if orderIDs:
3385            if allOrdersIDs is None:
3386                rawOrders = self.RequestPendingOrders()
3387                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3388
3389            if allStopOrdersIDs is None:
3390                rawStopOrders = self.RequestStopOrders()
3391                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3392
3393            for orderID in orderIDs:
3394                idInPendingOrders = orderID in allOrdersIDs
3395                idInStopOrders = orderID in allStopOrdersIDs
3396
3397                if not (idInPendingOrders or idInStopOrders):
3398                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3399                    continue
3400
3401                else:
3402                    if idInPendingOrders:
3403                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3404
3405                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3406                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3407                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3408                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3409
3410                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3411                            if self.moreDebug:
3412                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3413
3414                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3415
3416                        else:
3417                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3418
3419                    elif idInStopOrders:
3420                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3421
3422                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3423                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3424                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3425                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3426
3427                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3428                            if self.moreDebug:
3429                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3430
3431                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3432
3433                        else:
3434                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3435
3436                    else:
3437                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3439    def CloseAllOrders(self) -> None:
3440        """
3441        Gets a list of open pending and stop orders and cancel it all.
3442        """
3443        rawOrders = self.RequestPendingOrders()
3444        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3445        lenOrders = len(allOrdersIDs)
3446
3447        rawStopOrders = self.RequestStopOrders()
3448        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3449        lenSOrders = len(allStopOrdersIDs)
3450
3451        if lenOrders > 0 or lenSOrders > 0:
3452            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3453
3454            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3455
3456        else:
3457            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3459    def CloseAll(self, *args) -> None:
3460        """
3461        Close all available (not blocked) opened trades and orders.
3462
3463        Also, you can select one or more keywords case-insensitive:
3464        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3465
3466        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3467        """
3468        overview = self.Overview(show=False)  # get all open trades info
3469
3470        if len(args) == 0:
3471            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3472            self.CloseAllOrders()  # close all pending and stop orders
3473
3474            for iType in TKS_INSTRUMENTS:
3475                if iType != "Currencies":
3476                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3477
3478        else:
3479            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3480            lowerArgs = [x.lower() for x in args]
3481
3482            if "orders" in lowerArgs:
3483                self.CloseAllOrders()  # close all pending and stop orders
3484
3485            for iType in TKS_INSTRUMENTS:
3486                if iType.lower() in lowerArgs and iType != "Currencies":
3487                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3489    def CloseAllByTicker(self, instrument: str) -> None:
3490        """
3491        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3492
3493        This method searches opened trade and orders of instrument throw all portfolio and then use
3494        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3495
3496        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3497
3498        :param instrument: string with ticker.
3499        """
3500        if instrument is None or not instrument:
3501            uLogger.error("Ticker name must be defined for using this method!")
3502            raise Exception("Ticker required")
3503
3504        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3505
3506        self._ticker = instrument  # try to set instrument as ticker
3507        self._figi = ""
3508
3509        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3510        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3511
3512        if limitAll and self.IsInLimitOrders(portfolio=overview):
3513            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3514            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3515
3516        if stopAll and self.IsInStopOrders(portfolio=overview):
3517            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3518            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3519
3520        if self.IsInPortfolio(portfolio=overview):
3521            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3522            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3524    def CloseAllByFIGI(self, instrument: str) -> None:
3525        """
3526        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3527
3528        This method searches opened trade and orders of instrument throw all portfolio and then use
3529        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3530
3531        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3532
3533        :param instrument: string with FIGI id.
3534        """
3535        if instrument is None or not instrument:
3536            uLogger.error("FIGI id must be defined for using this method!")
3537            raise Exception("FIGI required")
3538
3539        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3540
3541        self._ticker = ""
3542        self._figi = instrument  # try to set instrument as FIGI id
3543
3544        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3545        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3546
3547        if limitAll and self.IsInLimitOrders(portfolio=overview):
3548            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3549            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3550
3551        if stopAll and self.IsInStopOrders(portfolio=overview):
3552            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3553            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3554
3555        if self.IsInPortfolio(portfolio=overview):
3556            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3557            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3559    @staticmethod
3560    def ParseOrderParameters(operation, **inputParameters):
3561        """
3562        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3563
3564        :param operation: string "Buy" or "Sell".
3565        :param inputParameters: this is dict of strings that looks like this
3566               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3567               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3568               "prices" key: one or more prices to open limit-orders
3569               Counts of values in lots and prices lists must be equals!
3570        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3571        """
3572        # TODO: update order grid work with api v2
3573        pass
3574        # uLogger.debug("Input parameters: {}".format(inputParameters))
3575        #
3576        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3577        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3578        #     raise Exception("Incorrect value")
3579        #
3580        # if "l" in inputParameters.keys():
3581        #     inputParameters["lots"] = inputParameters.pop("l")
3582        #
3583        # if "p" in inputParameters.keys():
3584        #     inputParameters["prices"] = inputParameters.pop("p")
3585        #
3586        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3587        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3588        #     raise Exception("Incorrect value")
3589        #
3590        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3591        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3592        #
3593        # if len(lots) != len(prices):
3594        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3595        #     raise Exception("Incorrect value")
3596        #
3597        # uLogger.debug("Extracted parameters for orders:")
3598        # uLogger.debug("lots = {}".format(lots))
3599        # uLogger.debug("prices = {}".format(prices))
3600        #
3601        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3602        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3603        # uLogger.debug("Order parameters: {}".format(result))
3604        #
3605        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3607    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3608        """
3609        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3610
3611        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3612        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3613        """
3614        result = False
3615        msg = "Instrument not defined!"
3616
3617        if portfolio is None or not portfolio:
3618            portfolio = self.Overview(show=False)
3619
3620        if self._ticker:
3621            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3622            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3623
3624            for iType in TKS_INSTRUMENTS:
3625                for instrument in portfolio["stat"][iType]:
3626                    if instrument["ticker"] == self._ticker:
3627                        result = True
3628                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3629                        break
3630
3631        elif self._figi:
3632            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3633            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3634
3635            for iType in TKS_INSTRUMENTS:
3636                for instrument in portfolio["stat"][iType]:
3637                    if instrument["figi"] == self._figi:
3638                        result = True
3639                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3640                        break
3641
3642        else:
3643            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3644
3645        uLogger.debug(msg)
3646
3647        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3649    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3650        """
3651        Returns instrument from the user's portfolio if it presents there.
3652        Instrument must be defined by `ticker` (highly priority) or `figi`.
3653
3654        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3655        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3656        """
3657        result = None
3658        msg = "Instrument not defined!"
3659
3660        if portfolio is None or not portfolio:
3661            portfolio = self.Overview(show=False)
3662
3663        if self._ticker:
3664            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3665            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3666
3667            for iType in TKS_INSTRUMENTS:
3668                for instrument in portfolio["stat"][iType]:
3669                    if instrument["ticker"] == self._ticker:
3670                        result = instrument
3671                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3672                        break
3673
3674        elif self._figi:
3675            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3676            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3677
3678            for iType in TKS_INSTRUMENTS:
3679                for instrument in portfolio["stat"][iType]:
3680                    if instrument["figi"] == self._figi:
3681                        result = instrument
3682                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3683                        break
3684
3685        else:
3686            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3687
3688        uLogger.debug(msg)
3689
3690        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3692    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3693        """
3694        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3695
3696        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3697
3698        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3699        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3700        """
3701        result = False
3702        msg = "Instrument not defined!"
3703
3704        if portfolio is None or not portfolio:
3705            portfolio = self.Overview(show=False)
3706
3707        if self._ticker:
3708            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3709            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3710
3711            for instrument in portfolio["stat"]["orders"]:
3712                if instrument["ticker"] == self._ticker:
3713                    result = True
3714                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3715                    break
3716
3717        elif self._figi:
3718            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3719            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3720
3721            for instrument in portfolio["stat"]["orders"]:
3722                if instrument["figi"] == self._figi:
3723                    result = True
3724                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3725                    break
3726
3727        else:
3728            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3729
3730        uLogger.debug(msg)
3731
3732        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3734    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3735        """
3736        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3737        Instrument must be defined by `ticker` (highly priority) or `figi`.
3738
3739        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3740
3741        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3742        :return: list with `orderID`s of limit orders.
3743        """
3744        result = []
3745        msg = "Instrument not defined!"
3746
3747        if portfolio is None or not portfolio:
3748            portfolio = self.Overview(show=False)
3749
3750        if self._ticker:
3751            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3752            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3753
3754            for instrument in portfolio["stat"]["orders"]:
3755                if instrument["ticker"] == self._ticker:
3756                    result.append(instrument["orderID"])
3757
3758            if result:
3759                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3760
3761        elif self._figi:
3762            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3763            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3764
3765            for instrument in portfolio["stat"]["orders"]:
3766                if instrument["figi"] == self._figi:
3767                    result.append(instrument["orderID"])
3768
3769            if result:
3770                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3771
3772        else:
3773            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3774
3775        uLogger.debug(msg)
3776
3777        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3779    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3780        """
3781        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3782
3783        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3784
3785        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3786        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3787        """
3788        result = False
3789        msg = "Instrument not defined!"
3790
3791        if portfolio is None or not portfolio:
3792            portfolio = self.Overview(show=False)
3793
3794        if self._ticker:
3795            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3796            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3797
3798            for instrument in portfolio["stat"]["stopOrders"]:
3799                if instrument["ticker"] == self._ticker:
3800                    result = True
3801                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3802                    break
3803
3804        elif self._figi:
3805            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3806            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3807
3808            for instrument in portfolio["stat"]["stopOrders"]:
3809                if instrument["figi"] == self._figi:
3810                    result = True
3811                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3812                    break
3813
3814        else:
3815            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3816
3817        uLogger.debug(msg)
3818
3819        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3821    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3822        """
3823        Returns list with all `orderID`s of opened stop orders for the instrument.
3824        Instrument must be defined by `ticker` (highly priority) or `figi`.
3825
3826        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3827
3828        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3829        :return: list with `orderID`s of stop orders.
3830        """
3831        result = []
3832        msg = "Instrument not defined!"
3833
3834        if portfolio is None or not portfolio:
3835            portfolio = self.Overview(show=False)
3836
3837        if self._ticker:
3838            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3839            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3840
3841            for instrument in portfolio["stat"]["stopOrders"]:
3842                if instrument["ticker"] == self._ticker:
3843                    result.append(instrument["orderID"])
3844
3845            if result:
3846                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3847
3848        elif self._figi:
3849            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3850            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3851
3852            for instrument in portfolio["stat"]["stopOrders"]:
3853                if instrument["figi"] == self._figi:
3854                    result.append(instrument["orderID"])
3855
3856            if result:
3857                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3858
3859        else:
3860            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3861
3862        uLogger.debug(msg)
3863
3864        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3866    def RequestLimits(self) -> dict:
3867        """
3868        Method for obtaining the available funds for withdrawal for current `accountId`.
3869
3870        See also:
3871        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3872        - `OverviewLimits()` method
3873
3874        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3875                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3876                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3877                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3878        """
3879        if self.accountId is None or not self.accountId:
3880            uLogger.error("Variable `accountId` must be defined for using this method!")
3881            raise Exception("Account ID required")
3882
3883        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3884
3885        self.body = str({"accountId": self.accountId})
3886        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3887        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3888
3889        if self.moreDebug:
3890            uLogger.debug("Records about available funds for withdrawal successfully received")
3891
3892        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3894    def OverviewLimits(self, show: bool = False) -> dict:
3895        """
3896        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3897
3898        See also: `RequestLimits()`.
3899
3900        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3901        :return: dict with raw parsed data from server and some calculated statistics about it.
3902        """
3903        if self.accountId is None or not self.accountId:
3904            uLogger.error("Variable `accountId` must be defined for using this method!")
3905            raise Exception("Account ID required")
3906
3907        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3908
3909        view = {
3910            "rawLimits": rawLimits,
3911            "limits": {  # parsed data for every currency:
3912                "money": {  # this is an array of portfolio currency positions
3913                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3914                },
3915                "blocked": {  # this is an array of blocked currency
3916                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3917                },
3918                "blockedGuarantee": {  # this is locked money under collateral for futures
3919                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3920                },
3921            },
3922        }
3923
3924        # --- Prepare text table with limits in human-readable format:
3925        if show:
3926            info = [
3927                "# Withdrawal limits\n\n",
3928                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3929                "* **Account ID:** [{}]\n".format(self.accountId),
3930            ]
3931
3932            if view["limits"]["money"]:
3933                info.extend([
3934                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3935                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3936                ])
3937
3938            else:
3939                info.append("\nNo withdrawal limits\n")
3940
3941            for curr in view["limits"]["money"].keys():
3942                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3943                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3944                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3945
3946                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3947                    "[{}]".format(curr),
3948                    "{:.2f}".format(view["limits"]["money"][curr]),
3949                    "{:.2f}".format(availableMoney),
3950                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3951                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3952                )
3953
3954                if curr == "rub":
3955                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3956
3957                else:
3958                    info.append(infoStr)
3959
3960            infoText = "".join(info)
3961
3962            uLogger.info(infoText)
3963
3964            if self.withdrawalLimitsFile:
3965                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3966                    fH.write(infoText)
3967
3968                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3969
3970                if self.useHTMLReports:
3971                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3972                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3973                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3974
3975                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3976
3977        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3979    def RequestAccounts(self) -> dict:
3980        """
3981        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3982
3983        See also:
3984        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3985        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3986        - `OverviewUserInfo()` method
3987
3988        :return: dict with raw data from server that contains accounts info. Example of dict:
3989                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3990                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3991                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3992                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3993        """
3994        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3995
3996        self.body = str({})
3997        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3998        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3999
4000        if self.moreDebug:
4001            uLogger.debug("Records about available accounts successfully received")
4002
4003        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4005    def RequestUserInfo(self) -> dict:
4006        """
4007        Method for requesting common user's information.
4008
4009        See also:
4010        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4011        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4012        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4013        - `OverviewUserInfo()` method
4014
4015        :return: dict with raw data from server that contains user's information. Example of dict:
4016                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4017                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4018        """
4019        uLogger.debug("Requesting common user's information. Wait, please...")
4020
4021        self.body = str({})
4022        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4023        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4024
4025        if self.moreDebug:
4026            uLogger.debug("Records about current user successfully received")
4027
4028        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4030    def RequestMarginStatus(self, accountId: str = None) -> dict:
4031        """
4032        Method for requesting margin calculation for defined account ID.
4033
4034        See also:
4035        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4036        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4037        - `OverviewUserInfo()` method
4038
4039        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4040        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4041                 Example of responses:
4042                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4043                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4044                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4045                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4046                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4047                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4048        """
4049        if accountId is None or not accountId:
4050            if self.accountId is None or not self.accountId:
4051                uLogger.error("Variable `accountId` must be defined for using this method!")
4052                raise Exception("Account ID required")
4053
4054            else:
4055                accountId = self.accountId  # use `self.accountId` (main ID) by default
4056
4057        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4058
4059        self.body = str({"accountId": accountId})
4060        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4061        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4062
4063        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4064            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4065            rawMargin = {}
4066
4067        else:
4068            if self.moreDebug:
4069                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4070
4071        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4073    def RequestTariffLimits(self) -> dict:
4074        """
4075        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4076
4077        See also:
4078        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4079        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4080        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4081        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4082        - `OverviewUserInfo()` method
4083
4084        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4085                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4086                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4087        """
4088        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4089
4090        self.body = str({})
4091        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4092        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4093
4094        if self.moreDebug:
4095            uLogger.debug("Records with limits of current tariff successfully received")
4096
4097        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4099    def RequestBondCoupons(self, iJSON: dict) -> dict:
4100        """
4101        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4102        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4103        All dates are in UTC timezone.
4104
4105        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4106        Documentation:
4107        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4108        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4109
4110        See also: `ExtendBondsData()`.
4111
4112        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4113                      If raw iJSON is not data of bond then server returns an error [400] with message:
4114                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4115        :return: dictionary with bond payment calendar. Response example
4116                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4117                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4118                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4119                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4120        """
4121        if iJSON["figi"] is None or not iJSON["figi"]:
4122            uLogger.error("FIGI must be defined for using this method!")
4123            raise Exception("FIGI required")
4124
4125        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4126        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4127
4128        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4129            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4130            self._figi,
4131            startDate,
4132            endDate,
4133        ))
4134
4135        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4136        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4137        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4138
4139        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4140            uLogger.warning("Instrument type is not bond!")
4141
4142        else:
4143            if self.moreDebug:
4144                uLogger.debug("Records about bond payment calendar successfully received")
4145
4146        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4148    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4149        """
4150        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4151        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4152        coupon yields, current yields and some statistics etc.
4153
4154        WARNING! This is too long operation if a lot of bonds requested from broker server.
4155
4156        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4157
4158        :param instruments: list of strings with tickers or FIGIs.
4159        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4160                     for further used by data scientists or stock analytics.
4161        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4162                 In XLSX-file and Pandas DataFrame fields mean:
4163                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4164                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4165        """
4166        if instruments is None or not instruments:
4167            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4168            raise Exception("Ticker or FIGI required")
4169
4170        if isinstance(instruments, str):
4171            instruments = [instruments]
4172
4173        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4174
4175        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4176
4177        iCount = len(uniqueInstruments)
4178        tooLong = iCount >= 20
4179        if tooLong:
4180            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4181
4182        bonds = None
4183        for i, self._figi in enumerate(uniqueInstruments):
4184            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4185
4186            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4187                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4188                rawBond = self.SearchByFIGI(requestPrice=True)
4189
4190                # Widen raw data with UTC current time (iData["actualDateTime"]):
4191                actualDate = datetime.now(tzutc())
4192                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4193
4194                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4195                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4196
4197                # Replace some values with human-readable:
4198                iData["nominalCurrency"] = iData["nominal"]["currency"]
4199                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4200                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4201                iData["aciCurrency"] = iData["aciValue"]["currency"]
4202                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4203                iData["issueSize"] = int(iData["issueSize"])
4204                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4205                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4206                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4207                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4208                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4209                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4210                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4211                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4212                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4213                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4214
4215                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4216                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4217                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4218                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4219                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4220                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4221                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4222                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4223                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4224                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4225                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4226
4227                # Widen raw data with calendar data from `rawCalendar` values:
4228                calendarData = []
4229                if "events" in iData["rawCalendar"].keys():
4230                    for item in iData["rawCalendar"]["events"]:
4231                        calendarData.append({
4232                            "couponDate": item["couponDate"],
4233                            "couponNumber": int(item["couponNumber"]),
4234                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4235                            "payCurrency": item["payOneBond"]["currency"],
4236                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4237                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4238                            "couponStartDate": item["couponStartDate"],
4239                            "couponEndDate": item["couponEndDate"],
4240                            "couponPeriod": item["couponPeriod"],
4241                        })
4242
4243                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4244                    if "maturityDate" not in iData.keys():
4245                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4246
4247                # Widen raw data with Coupon Rate.
4248                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4249                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4250                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4251                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4252
4253                # Widen raw data with Yield to Maturity (YTM) on current date.
4254                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4255                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4256                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4257                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4258                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4259                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4260
4261                iData["calendar"] = calendarData  # adds calendar at the end
4262
4263                # Remove not used data:
4264                iData.pop("uid")
4265                iData.pop("positionUid")
4266                iData.pop("currentPrice")
4267                iData.pop("rawCalendar")
4268
4269                colNames = list(iData.keys())
4270                if bonds is None:
4271                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4272
4273                else:
4274                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4275
4276            else:
4277                uLogger.warning("Instrument is not a bond!")
4278
4279            processed = round(100 * (i + 1) / iCount, 1)
4280            if tooLong and processed % 5 == 0:
4281                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4282
4283            else:
4284                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4285
4286        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4287
4288        # Saving bonds from Pandas DataFrame to XLSX sheet:
4289        if xlsx and self.bondsXLSXFile:
4290            with pd.ExcelWriter(
4291                    path=self.bondsXLSXFile,
4292                    date_format=TKS_DATE_FORMAT,
4293                    datetime_format=TKS_DATE_TIME_FORMAT,
4294                    mode="w",
4295            ) as writer:
4296                bonds.to_excel(
4297                    writer,
4298                    sheet_name="Extended bonds data",
4299                    index=True,
4300                    encoding="UTF-8",
4301                    freeze_panes=(1, 1),
4302                )  # saving as XLSX-file with freeze first row and column as headers
4303
4304            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4305
4306        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4308    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4309        """
4310        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4311
4312        WARNING! This is too long operation if a lot of bonds requested from broker server.
4313
4314        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4315
4316        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4317                        extended information about bonds: main info, current prices, bond payment calendar,
4318                        coupon yields, current yields and some statistics etc.
4319                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4320        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4321                     for further used by data scientists or stock analytics.
4322        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4323        """
4324        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4325            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4326
4327        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4328
4329        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4330        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4331        calendar = None
4332        for bond in extBonds.iterrows():
4333            for item in bond[1]["calendar"]:
4334                cData = {
4335                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4336                    "couponDate": item["couponDate"],
4337                    "figi": bond[1]["figi"],
4338                    "ticker": bond[1]["ticker"],
4339                    "name": bond[1]["name"],
4340                    "couponNumber": item["couponNumber"],
4341                    "payOneBond": item["payOneBond"],
4342                    "payCurrency": item["payCurrency"],
4343                    "couponType": item["couponType"],
4344                    "couponPeriod": item["couponPeriod"],
4345                    "fixDate": item["fixDate"],
4346                    "couponStartDate": item["couponStartDate"],
4347                    "couponEndDate": item["couponEndDate"],
4348                }
4349
4350                if calendar is None:
4351                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4352
4353                else:
4354                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4355
4356        if calendar is not None:
4357            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4358
4359            # Saving calendar from Pandas DataFrame to XLSX sheet:
4360            if xlsx:
4361                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4362
4363                with pd.ExcelWriter(
4364                        path=xlsxCalendarFile,
4365                        date_format=TKS_DATE_FORMAT,
4366                        datetime_format=TKS_DATE_TIME_FORMAT,
4367                        mode="w",
4368                ) as writer:
4369                    humanReadable = calendar.copy(deep=True)
4370                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4371                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4372                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4373                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4374                    humanReadable.columns = colNames  # human-readable column names
4375
4376                    humanReadable.to_excel(
4377                        writer,
4378                        sheet_name="Bond payments calendar",
4379                        index=False,
4380                        encoding="UTF-8",
4381                        freeze_panes=(1, 2),
4382                    )  # saving as XLSX-file with freeze first row and column as headers
4383
4384                    del humanReadable  # release df in memory
4385
4386                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4387
4388        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4390    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4391        """
4392        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4393        Also, creates Markdown file with calendar data, `calendar.md` by default.
4394
4395        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4396
4397        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4398                        extended information about bonds: main info, current prices, bond payment calendar,
4399                        coupon yields, current yields and some statistics etc.
4400                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4401        :param show: if `True` then also printing bonds payment calendar to the console,
4402                     otherwise save to file `calendarFile` only. `False` by default.
4403        :return: multilines text in Markdown format with bonds payment calendar as a table.
4404        """
4405        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4406            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4407
4408        infoText = "# Bond payments calendar\n\n"
4409
4410        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4411
4412        if not (calendar is None or calendar.empty):
4413            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4414
4415            info = [
4416                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4417                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4418                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4419            ]
4420
4421            newMonth = False
4422            notOneBond = calendar["figi"].nunique() > 1
4423            for i, bond in enumerate(calendar.iterrows()):
4424                if newMonth and notOneBond:
4425                    info.append(splitLine)
4426
4427                info.append(
4428                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4429                        "  √" if bond[1]["paid"] else "  —",
4430                        bond[1]["couponDate"].split("T")[0],
4431                        bond[1]["figi"],
4432                        bond[1]["ticker"],
4433                        bond[1]["couponNumber"],
4434                        "{} {}".format(
4435                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4436                            bond[1]["payCurrency"],
4437                        ),
4438                        bond[1]["couponType"],
4439                        bond[1]["couponPeriod"],
4440                        bond[1]["fixDate"].split("T")[0],
4441                    )
4442                )
4443
4444                if i < len(calendar.values) - 1:
4445                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4446                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4447                    newMonth = False if curDate.month == nextDate.month else True
4448
4449                else:
4450                    newMonth = False
4451
4452            infoText += "".join(info)
4453
4454            if show:
4455                uLogger.info("{}".format(infoText))
4456
4457            if self.calendarFile is not None:
4458                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4459                    fH.write(infoText)
4460
4461                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4462
4463                if self.useHTMLReports:
4464                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4465                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4466                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4467
4468                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4469
4470        else:
4471            infoText += "No data\n"
4472
4473        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4475    def OverviewAccounts(self, show: bool = False) -> dict:
4476        """
4477        Method for parsing and show simple table with all available user accounts.
4478
4479        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4480
4481        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4482        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4483                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4484                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4485                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4486                                                        "closed": "—", "access": "Full access" }, ...}}`
4487        """
4488        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4489
4490        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4491        accounts = {
4492            item["id"]: {
4493                "type": TKS_ACCOUNT_TYPES[item["type"]],
4494                "name": item["name"],
4495                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4496                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4497                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4498                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4499            } for item in rawAccounts["accounts"]
4500        }
4501
4502        # Raw and parsed data with some fields replaced in "stat" section:
4503        view = {
4504            "rawAccounts": rawAccounts,
4505            "stat": accounts,
4506        }
4507
4508        # --- Prepare simple text table with only accounts data in human-readable format:
4509        if show:
4510            info = [
4511                "# User accounts\n\n",
4512                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4513                "| Account ID   | Type                      | Status                    | Name                           |\n",
4514                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4515            ]
4516
4517            for account in view["stat"].keys():
4518                info.extend([
4519                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4520                        account,
4521                        view["stat"][account]["type"],
4522                        view["stat"][account]["status"],
4523                        view["stat"][account]["name"],
4524                    )
4525                ])
4526
4527            infoText = "".join(info)
4528
4529            uLogger.info(infoText)
4530
4531            if self.userAccountsFile:
4532                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4533                    fH.write(infoText)
4534
4535                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4536
4537                if self.useHTMLReports:
4538                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4539                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4540                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4541
4542                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4543
4544        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4546    def OverviewUserInfo(self, show: bool = False) -> dict:
4547        """
4548        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4549
4550        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4551
4552        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4553        :return: dict with raw parsed data from server and some calculated statistics about it.
4554        """
4555        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4556        tmpTicker = self._ticker
4557        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4558        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4559        self._ticker = tmpTicker
4560
4561        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4562        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4563        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4564        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4565        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4566        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4567
4568        # This is dict with parsed common user data:
4569        userInfo = {
4570            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4571            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4572            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4573            "tariff": rawUserInfo["tariff"],
4574        }
4575
4576        # This is an array of dict with parsed margin statuses for every account IDs:
4577        margins = {}
4578        for accountId in accounts.keys():
4579            if rawMargins[accountId]:
4580                margins[accountId] = {
4581                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4582                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4583                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4584                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4585                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4586                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4587                    "missing": missing["volume"],
4588                }
4589
4590            else:
4591                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4592
4593        unary = {}  # unary-connection limits
4594        for item in rawTariffLimits["unaryLimits"]:
4595            if item["limitPerMinute"] in unary.keys():
4596                unary[item["limitPerMinute"]].extend(item["methods"])
4597
4598            else:
4599                unary[item["limitPerMinute"]] = item["methods"]
4600
4601        stream = {}  # stream-connection limits
4602        for item in rawTariffLimits["streamLimits"]:
4603            if item["limit"] in stream.keys():
4604                stream[item["limit"]].extend(item["streams"])
4605
4606            else:
4607                stream[item["limit"]] = item["streams"]
4608
4609        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4610        limits = {
4611            "unary": unary,
4612            "stream": stream,
4613        }
4614
4615        # Raw and parsed data as an output result:
4616        view = {
4617            "rawUserInfo": rawUserInfo,
4618            "rawAccounts": rawAccounts,
4619            "rawMargins": rawMargins,
4620            "rawTariffLimits": rawTariffLimits,
4621            "stat": {
4622                "overview": overview,
4623                "userInfo": userInfo,
4624                "accounts": accounts,
4625                "margins": margins,
4626                "limits": limits,
4627            },
4628        }
4629
4630        # --- Prepare text table with user information in human-readable format:
4631        if show:
4632            info = [
4633                "# Full user information\n\n",
4634                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4635                "## Common information\n\n",
4636                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4637                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4638                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4639                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4640                "\n## User accounts\n\n",
4641            ]
4642
4643            for account in view["stat"]["accounts"].keys():
4644                info.extend([
4645                    "### ID: [{}]\n\n".format(account),
4646                    "| Parameters           | Values                                                       |\n",
4647                    "|----------------------|--------------------------------------------------------------|\n",
4648                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4649                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4650                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4651                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4652                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4653                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4654                ])
4655
4656                if margins[account]:
4657                    info.extend([
4658                        "| Margin status:       | Enabled                                                      |\n",
4659                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4660                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4661                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4662                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4663                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4664                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4665                    ])
4666
4667                else:
4668                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4669
4670            info.extend([
4671                "\n## Current user tariff limits\n",
4672                "\n### See also\n",
4673                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4674                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4675                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4676                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4677                "\n### Unary limits\n",
4678            ])
4679
4680            if unary:
4681                for key, values in sorted(unary.items()):
4682                    info.append("\n* Max requests per minute: {}\n".format(key))
4683
4684                    for value in values:
4685                        info.append("  - {}\n".format(value))
4686
4687            else:
4688                info.append("\nNot available\n")
4689
4690            info.append("\n### Stream limits\n")
4691
4692            if stream:
4693                for key, values in sorted(stream.items()):
4694                    info.append("\n* Max stream connections: {}\n".format(key))
4695
4696                    for value in values:
4697                        info.append("  - {}\n".format(value))
4698
4699            else:
4700                info.append("\nNot available\n")
4701
4702            infoText = "".join(info)
4703
4704            uLogger.info(infoText)
4705
4706            if self.userInfoFile:
4707                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4708                    fH.write(infoText)
4709
4710                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4711
4712                if self.useHTMLReports:
4713                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4714                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4715                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4716
4717                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4718
4719        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4722class Args:
4723    """
4724    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4725    """
4726    def __init__(self, **kwargs):
4727        self.__dict__.update(kwargs)
4728
4729    def __getattr__(self, item):
4730        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4726    def __init__(self, **kwargs):
4727        self.__dict__.update(kwargs)
def ParseArgs():
4733def ParseArgs():
4734    """This function get and parse command line keys."""
4735    parser = ArgumentParser()  # command-line string parser
4736
4737    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4738    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4739
4740    # --- options:
4741
4742    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4743    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4744    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4745
4746    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4747    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4748
4749    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4750    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4751
4752    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4753    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4754
4755    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4756    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4757    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4758
4759    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4760    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4761
4762    # --- commands:
4763
4764    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4765
4766    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4767    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4768    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4769    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4770    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4771    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4772    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4773    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4774
4775    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4776    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4777    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4778    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4779    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4780    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4781
4782    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4783    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4784    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4785    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4786
4787    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4788    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4789    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4790
4791    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4792    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4793    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4794    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4795    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4796    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4797    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4798
4799    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4800    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4801    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4802    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4803    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4804
4805    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4806    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4807    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4808
4809    cmdArgs = parser.parse_args()
4810    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4813def Main(**kwargs):
4814    """
4815    Main function for work with TKSBrokerAPI in the console.
4816
4817    See examples:
4818    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4819    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4820    """
4821    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4822
4823    if args.debug_level:
4824        uLogger.level = 10  # always debug level by default
4825        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4826
4827    exitCode = 0
4828    start = datetime.now(tzutc())
4829    uLogger.debug("=-" * 50)
4830    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4831        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4832        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4833    ))
4834
4835    # trying to calculate full current version:
4836    buildVersion = __version__
4837    try:
4838        v = version("tksbrokerapi")
4839        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4840
4841    except Exception:
4842        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4843
4844    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4845    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4846
4847    try:
4848        if args.version:
4849            print("TKSBrokerAPI {}".format(buildVersion))
4850            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4851
4852        else:
4853            # Init class for trading with Tinkoff Broker:
4854            trader = TinkoffBrokerServer(
4855                token=args.token,
4856                accountId=args.account_id,
4857                useCache=not args.no_cache,
4858            )
4859
4860            # --- set some options:
4861
4862            if args.more:
4863                trader.moreDebug = True
4864                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4865
4866            if args.html:
4867                trader.useHTMLReports = True
4868
4869            if args.ticker:
4870                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4871
4872                if ticker in trader.aliasesKeys:
4873                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4874
4875                else:
4876                    trader.ticker = ticker
4877
4878            if args.figi:
4879                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4880
4881            if args.depth is not None:
4882                trader.depth = args.depth
4883
4884            # --- do one command:
4885
4886            if args.list:
4887                if args.output is not None:
4888                    trader.instrumentsFile = args.output
4889
4890                trader.ShowInstrumentsInfo(show=True)
4891
4892            elif args.list_xlsx:
4893                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4894
4895            elif args.bonds_xlsx is not None:
4896                if args.output is not None:
4897                    trader.bondsXLSXFile = args.output
4898
4899                if len(args.bonds_xlsx) == 0:
4900                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4901
4902                else:
4903                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4904
4905            elif args.search:
4906                if args.output is not None:
4907                    trader.searchResultsFile = args.output
4908
4909                trader.SearchInstruments(pattern=args.search[0], show=True)
4910
4911            elif args.info:
4912                if not (args.ticker or args.figi):
4913                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4914                    raise Exception("Ticker or FIGI required")
4915
4916                if args.output is not None:
4917                    trader.infoFile = args.output
4918
4919                if args.ticker:
4920                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4921
4922                else:
4923                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4924
4925            elif args.calendar is not None:
4926                if args.output is not None:
4927                    trader.calendarFile = args.output
4928
4929                if len(args.calendar) == 0:
4930                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4931
4932                else:
4933                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4934
4935                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4936
4937            elif args.price:
4938                if not (args.ticker or args.figi):
4939                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4940                    raise Exception("Ticker or FIGI required")
4941
4942                trader.GetCurrentPrices(show=True)
4943
4944            elif args.prices is not None:
4945                if args.output is not None:
4946                    trader.pricesFile = args.output
4947
4948                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4949
4950            elif args.overview:
4951                if args.output is not None:
4952                    trader.overviewFile = args.output
4953
4954                trader.Overview(show=True, details="full")
4955
4956            elif args.overview_digest:
4957                if args.output is not None:
4958                    trader.overviewDigestFile = args.output
4959
4960                trader.Overview(show=True, details="digest")
4961
4962            elif args.overview_positions:
4963                if args.output is not None:
4964                    trader.overviewPositionsFile = args.output
4965
4966                trader.Overview(show=True, details="positions")
4967
4968            elif args.overview_orders:
4969                if args.output is not None:
4970                    trader.overviewOrdersFile = args.output
4971
4972                trader.Overview(show=True, details="orders")
4973
4974            elif args.overview_analytics:
4975                if args.output is not None:
4976                    trader.overviewAnalyticsFile = args.output
4977
4978                trader.Overview(show=True, details="analytics")
4979
4980            elif args.overview_calendar:
4981                if args.output is not None:
4982                    trader.overviewAnalyticsFile = args.output
4983
4984                trader.Overview(show=True, details="calendar")
4985
4986            elif args.deals is not None:
4987                if args.output is not None:
4988                    trader.reportFile = args.output
4989
4990                if 0 <= len(args.deals) < 3:
4991                    trader.Deals(
4992                        start=args.deals[0] if len(args.deals) >= 1 else None,
4993                        end=args.deals[1] if len(args.deals) == 2 else None,
4994                        show=True,  # Always show deals report in console
4995                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4996                    )
4997
4998                else:
4999                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5000                    raise Exception("Incorrect value")
5001
5002            elif args.history is not None:
5003                if args.output is not None:
5004                    trader.historyFile = args.output
5005
5006                if 0 <= len(args.history) < 3:
5007                    dataReceived = trader.History(
5008                        start=args.history[0] if len(args.history) >= 1 else None,
5009                        end=args.history[1] if len(args.history) == 2 else None,
5010                        interval="hour" if args.interval is None or not args.interval else args.interval,
5011                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5012                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5013                        show=True,  # shows all downloaded candles in console
5014                    )
5015
5016                    if args.render_chart is not None and dataReceived is not None:
5017                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5018
5019                        trader.ShowHistoryChart(
5020                            candles=dataReceived,
5021                            interact=iChart,
5022                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5023                        )
5024
5025                else:
5026                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5027                    raise Exception("Incorrect value")
5028
5029            elif args.load_history is not None:
5030                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5031
5032                if args.render_chart is not None and histData is not None:
5033                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5034                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5035
5036                    trader.ShowHistoryChart(
5037                        candles=histData,
5038                        interact=iChart,
5039                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5040                    )
5041
5042            elif args.trade is not None:
5043                if 1 <= len(args.trade) <= 5:
5044                    trader.Trade(
5045                        operation=args.trade[0],
5046                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5047                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5048                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5049                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5050                    )
5051
5052                else:
5053                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5054
5055            elif args.buy is not None:
5056                if 0 <= len(args.buy) <= 4:
5057                    trader.Buy(
5058                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5059                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5060                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5061                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5062                    )
5063
5064                else:
5065                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5066
5067            elif args.sell is not None:
5068                if 0 <= len(args.sell) <= 4:
5069                    trader.Sell(
5070                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5071                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5072                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5073                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5074                    )
5075
5076                else:
5077                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5078
5079            elif args.order:
5080                if 4 <= len(args.order) <= 7:
5081                    trader.Order(
5082                        operation=args.order[0],
5083                        orderType=args.order[1],
5084                        lots=int(args.order[2]),
5085                        targetPrice=float(args.order[3]),
5086                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5087                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5088                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5089                    )
5090
5091                else:
5092                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5093
5094            elif args.buy_limit:
5095                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5096
5097            elif args.sell_limit:
5098                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5099
5100            elif args.buy_stop:
5101                if 2 <= len(args.buy_stop) <= 7:
5102                    trader.BuyStop(
5103                        lots=int(args.buy_stop[0]),
5104                        targetPrice=float(args.buy_stop[1]),
5105                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5106                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5107                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5108                    )
5109
5110                else:
5111                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5112
5113            elif args.sell_stop:
5114                if 2 <= len(args.sell_stop) <= 7:
5115                    trader.SellStop(
5116                        lots=int(args.sell_stop[0]),
5117                        targetPrice=float(args.sell_stop[1]),
5118                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5119                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5120                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5121                    )
5122
5123                else:
5124                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5125
5126            # elif args.buy_order_grid is not None:
5127            #     # update order grid work with api v2
5128            #     if len(args.buy_order_grid) == 2:
5129            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5130            #
5131            #         for order in orderParams:
5132            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5133            #
5134            #     else:
5135            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5136            #
5137            # elif args.sell_order_grid is not None:
5138            #     # update order grid work with api v2
5139            #     if len(args.sell_order_grid) >= 2:
5140            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5141            #
5142            #         for order in orderParams:
5143            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5144            #
5145            #     else:
5146            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5147
5148            elif args.close_order is not None:
5149                trader.CloseOrders(args.close_order)  # close only one order
5150
5151            elif args.close_orders is not None:
5152                trader.CloseOrders(args.close_orders)  # close list of orders
5153
5154            elif args.close_trade:
5155                if not (args.ticker or args.figi):
5156                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5157                    raise Exception("Ticker or FIGI required")
5158
5159                if args.ticker:
5160                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5161
5162                else:
5163                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5164
5165            elif args.close_trades is not None:
5166                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5167
5168            elif args.close_all is not None:
5169                if args.ticker:
5170                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5171
5172                elif args.figi:
5173                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5174
5175                else:
5176                    trader.CloseAll(*args.close_all)
5177
5178            elif args.limits:
5179                if args.output is not None:
5180                    trader.withdrawalLimitsFile = args.output
5181
5182                trader.OverviewLimits(show=True)
5183
5184            elif args.user_info:
5185                if args.output is not None:
5186                    trader.userInfoFile = args.output
5187
5188                trader.OverviewUserInfo(show=True)
5189
5190            elif args.account:
5191                if args.output is not None:
5192                    trader.userAccountsFile = args.output
5193
5194                trader.OverviewAccounts(show=True)
5195
5196            else:
5197                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5198                raise Exception("There is no command to execute")
5199
5200    except Exception:
5201        trace = tb.format_exc()
5202        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5203            if e in trace:
5204                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5205                break
5206
5207        uLogger.debug(trace)
5208        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5209        exitCode = 255  # an error occurred, must be open a ticket for this issue
5210
5211    finally:
5212        finish = datetime.now(tzutc())
5213
5214        if exitCode == 0:
5215            if args.more:
5216                uLogger.debug("All operations were finished success (summary code is 0).")
5217
5218        else:
5219            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5220                os.path.abspath(uLog.defaultLogFile), exitCode,
5221            ))
5222
5223        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5224        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5225            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5226            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5227        ))
5228        uLogger.debug("=-" * 50)
5229
5230        if not kwargs:
5231            sys.exit(exitCode)
5232
5233        else:
5234            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: